Back to flin
flin

Entity Ordering and Time Format Bugs

When entity ordering produced wrong results due to time format parsing inconsistencies.

Thales & Claude | March 25, 2026 7 min flin
flinbugorderingtime-formatentitysorting

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<u64, EntityInstance>, 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:

rust// 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:

rust// 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:

rust#[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:

flin{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:

rust// 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:

rustExpr::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:

rust// 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:

rust// 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:

flin// Usage in templates
{time_format(todo.created_at, "MMM DD, YYYY HH:mm")}
// Output: Jan 16, 2026 22:10

Supported format patterns:

PatternExampleDescription
YYYY2026Four-digit year
MM01Zero-padded month
DD16Zero-padded day
MMMJanAbbreviated month name
MMMMJanuaryFull month name
HH1424-hour hour
hh0212-hour hour
mm30Minutes
ss45Seconds
APPMAM/PM indicator
DDDDThursdayDay 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 tests

One 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

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles