Back to flin
flin

#167 -- Entity Ordering and Time Format Bugs

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

Juste A. Gnimavo (Thales) & Claude | March 26, 2026 7 min flin
EN/ FR/ ES
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

Thales & Claude thales

Thirteen Agents, Forty-Three Minutes: The First Claude Fable 5 Workflow Session, And What A Deterministic Orchestration Script Changes About Multi-Agent Builds

One prompt, thirteen agents, forty-three minutes: the first production session with Claude Fable 5 and Claude Code's Workflow tool shipped a complete seven-page production website plus a backend lead-capture endpoint in a single commit. The build log: the deterministic orchestration script, the contract-injection pattern between phases, the per-agent economics of the parallel fan-out, and the session-limit cliffhanger the resume journal turned into a non-event.

20 min Jun 12, 2026
claude-fable-5claude-codeworkflow-toolmulti-agent +10
Thales & Claude casp

The gate caught its own drift: one day inside CASP with Claude Fable 5

We handed the most autonomous Claude model yet the keys to CASP — the open-source CLI that keeps AI coding agents honest against git — with the authority to reject our own roadmap. It rejected five things, found two real bugs in the validator by dogfooding it, fixed them under a two-auditor gate, and left casp check fully green on its own repo for the first time. CASP 0.3.0 is the result.

14 min Jun 10, 2026
caspzerosuiteworkflowai-cto +9
Thales & Claude zerosuite

The CASP Transplant: How The Six-File Discipline Moved From Conductor To An Anti-Fraud Transport ERP, What The /next Skill Adds When The Operator Just Types 'next', And Why The Cost Of CASP Drift Rises When The Project Is Someone Else's Cash

The CASP discipline that ran thirty-five Conductor sessions is product-agnostic. The build log of transplanting it to KASSIA, an anti-fraud transport ERP for a Côte d'Ivoire fleet operator: what moved, what did not (the bespoke validator — and what its absence costs), what the /next skill adds when the operator types one word, and where the CASP stops — the deployment bug it could not see because it records intent, not infrastructure reality.

20 min Jun 8, 2026
kassiaerp-kassia-transport-logistiquezerosuiteCASP +15