January 16, 2026, Session 206, was a double-bug session. Two unrelated issues conspired to make the todo application look broken: entities appeared in random order after refresh, and time formatting functions returned nothing in templates. Both bugs had straightforward fixes, but their combination created an experience where everything seemed wrong even though each individual component worked correctly in isolation.
Bug One: The HashMap Shuffle
After the persistence fixes of Sessions 201-203, the todo application finally saved data to disk. But users noticed a disorienting behavior: every time the page refreshed, the todos appeared in a different order.
Create three tasks: "Buy groceries," "Walk the dog," "Read a book." Refresh the page. They might appear as "Read a book," "Buy groceries," "Walk the dog." Refresh again: "Walk the dog," "Read a book," "Buy groceries." Every refresh shuffled the deck.
Root Cause
The ZeroCore database stored entities in a HashMap, keyed by entity ID. HashMaps in Rust, like most hash table implementations, provide no ordering guarantees. Iteration order depends on hash values, bucket distribution, and table capacity -- all of which can change between program runs due to random hash seeding.
The all() method simply collected the HashMap's values:
// BEFORE: Random order each time
pub fn all(&self, entity_type: &str) -> Vec<EntityInstance> {
self.entities
.get(entity_type)
.map(|map| map.values().cloned().collect())
.unwrap_or_default()
}The Fix
The fix added a sort step after collection:
// AFTER: Consistent order by creation time
pub fn all(&self, entity_type: &str) -> Vec<EntityInstance> {
let mut entities: Vec<EntityInstance> = self.entities
.get(entity_type)
.map(|map| map.values().cloned().collect())
.unwrap_or_default();// Sort by (created_at, id) for consistent ordering entities.sort_by_key(|e| (e.created_at, e.id));
entities } ```
The sort key uses both created_at (timestamp) and id (sequential integer). Using both fields ensures stable ordering even when multiple entities are created within the same millisecond -- the ID serves as a tiebreaker.
The same fix was applied to all_including_deleted(), which is used by admin interfaces to show soft-deleted entities alongside active ones.
A test was added to verify the fix:
#[test]
fn test_all_preserves_insertion_order() {
let mut db = ZeroCore::new();
// Create entities with slight time gaps
for i in 0..10 {
db.save("Task", None, fields!{ "title" => format!("Task {}", i) });
}
let tasks = db.all("Task");
for (i, task) in tasks.iter().enumerate() {
assert_eq!(task.fields["title"], format!("Task {}", i));
}
}Bug Two: Template Function Calls Return None
With ordering fixed, we wanted to display timestamps on each todo. The format was straightforward:
{time_format(todo.created_at, "MMM DD, YYYY HH:mm")}This should display something like "Jan 16, 2026 22:10." Instead, it displayed nothing. The function call evaluated to None in the template context.
Root Cause
The renderer's eval_expr_with_scope() function did not handle Expr::Call:
// src/vm/renderer.rs:1364
match expr {
Expr::Identifier { name, .. } => { /* look up variable */ }
Expr::FieldAccess { .. } => { /* access entity field */ }
Expr::Literal { .. } => { /* return literal value */ }
_ => Value::None, // ALL unhandled expressions return None
}Function calls fell into the catch-all _ branch and returned None. The renderer could display variables and field access but could not evaluate function calls.
Why the Renderer Is Special
The VM can execute function calls -- that is its primary purpose. But the renderer operates differently. It walks the view AST (Abstract Syntax Tree) and evaluates expressions to produce HTML strings. It does not execute bytecode; it evaluates AST nodes directly.
This means the renderer needs its own expression evaluator, separate from the VM's bytecode execution. And that evaluator needs to handle every expression type that can appear in a template, including function calls.
The Fix
We added function call evaluation to the renderer for built-in functions:
Expr::Call { callee, args, .. } => {
if let Expr::Identifier { name, .. } = callee.as_ref() {
let evaluated_args: Vec<Value> = args.iter()
.map(|a| eval_expr_with_scope(a, vm, scope))
.collect();match name.as_str() { // Time functions "time_format" => { let ts = evaluated_args.get(0) .and_then(|v| v.as_int()).unwrap_or(0); let pattern = evaluated_args.get(1) .and_then(|v| v.as_string()).unwrap_or_default(); Value::Text(format_timestamp(ts, &pattern)) } "time_year" | "time_month" | "time_day" | "time_hour" | "time_minute" | "time_second" => { let ts = evaluated_args.get(0) .and_then(|v| v.as_int()).unwrap_or(0); Value::Int(extract_time_component(name, ts)) } // String functions "uppercase" => { / ... / } "lowercase" => { / ... / } "trim" => { / ... / } "len" => { / ... / } // Conversion functions "to_string" => { / ... / } "to_int" => { / ... / } "to_float" => { / ... / } _ => Value::None, } } else { Value::None } } ```
Internal Entity Fields
For time_format(todo.created_at, ...) to work, todo.created_at needed to be accessible. But entity internal fields (id, created_at, updated_at, version, etc.) were not exposed through the renderer's field access.
We added special property handling for EntityInstance internal fields:
// src/vm/renderer.rs:1348-1368
match field_name.as_str() {
"id" => Value::Int(entity.id as i64),
"entity_type" => Value::Text(entity.entity_type.clone()),
"version" => Value::Int(entity.version as i64),
"created_at" => Value::Int(entity.created_at as i64),
"updated_at" => Value::Int(entity.updated_at as i64),
"deleted_at" => match entity.deleted_at {
Some(ts) => Value::Int(ts as i64),
None => Value::None,
},
"valid_from" => Value::Int(entity.valid_from as i64),
"valid_to" => match entity.valid_to {
Some(ts) => Value::Int(ts as i64),
None => Value::None,
},
_ => entity.fields.get(field_name).cloned().unwrap_or(Value::None),
}This exposed eight internal fields: id, entity_type, version, created_at, updated_at, deleted_at, valid_from, and valid_to. These are the temporal and metadata fields that ZeroCore manages automatically.
Type Checker Registration
The time functions also needed to be registered in the type checker to prevent "undefined function" errors:
// src/typechecker/checker.rs:3440-3505
"time_format" => Some(FlinType::Function {
params: vec![FlinType::Int, FlinType::Text],
ret: Box::new(FlinType::Text),
min_arity: 2,
has_rest: false,
}),
"time_year" | "time_month" | "time_day"
| "time_hour" | "time_minute" | "time_second" => Some(FlinType::Function {
params: vec![FlinType::Int],
ret: Box::new(FlinType::Int),
min_arity: 1,
has_rest: false,
}),The Date Format System
With time formatting working, FLIN gained a comprehensive date format system:
// Usage in templates
{time_format(todo.created_at, "MMM DD, YYYY HH:mm")}
// Output: Jan 16, 2026 22:10Supported format patterns:
| Pattern | Example | Description |
|---|---|---|
YYYY | 2026 | Four-digit year |
MM | 01 | Zero-padded month |
DD | 16 | Zero-padded day |
MMM | Jan | Abbreviated month name |
MMMM | January | Full month name |
HH | 14 | 24-hour hour |
hh | 02 | 12-hour hour |
mm | 30 | Minutes |
ss | 45 | Seconds |
AP | PM | AM/PM indicator |
DDDD | Thursday | Day of week |
The philosophy matches PostgreSQL: store raw timestamps, format on display. Entities automatically get created_at and updated_at timestamps when saved. The developer formats them in templates using time_format().
Test Results
Both fixes landed cleanly:
Library tests: 2,249 passed (0 failed)
Integration tests: 617 passed (0 failed)
Total: 2,866 testsOne new test was added for entity ordering. The template function evaluation was tested manually in the browser.
The Two-Bug Pattern
Session 206 illustrates a common debugging pattern: two unrelated bugs that combine to create a confusing experience. Individually, each bug had a clear symptom:
- Random entity ordering: todos shuffle on refresh
- Missing time formatting: timestamps display as empty
But together, they made the application look fundamentally broken. The user saw shuffled, timestampless todos and concluded that the persistence system was malfunctioning. In reality, persistence was perfect -- the bugs were in presentation (ordering) and rendering (function calls).
This combination effect is why bug reports from users are often misleading. The user reports what they see ("my todos are broken"), not what is actually wrong ("HashMap iteration order is non-deterministic" and "template function calls are not evaluated"). The debugging process must separate the visible symptoms from the independent root causes.
---
This is Part 167 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.
Series Navigation: - [166] The Entity .get() Method Bug - [167] Entity Ordering and Time Format Bugs (you are here) - [168] Entity Defaults and Toggle Fix