Back to flin
flin

Function Audit Day 7 Complete

The exhaustive function-by-function audit of all 409+ built-in functions on day 7.

Thales & Claude | March 25, 2026 8 min flin
flinauditfunctionsbuilt-instandard-library

By the seventh day of the exhaustive audit, we had reached the built-in functions. FLIN's specification promises hundreds of native functions -- string manipulation, math operations, date predicates, list transformations, type checking, validation, formatting, and encoding. The question was not whether these functions existed in the Rust code. The question was whether they were accessible from where FLIN developers actually need them: inside templates.

The gap analysis revealed a startling asymmetry. At the bytecode level, FLIN had implemented approximately 95% of its specified functions -- 350+ native operations ready to execute. But at the template level, where developers write their UI logic, only 17 functions were accessible -- roughly 12% of what was available. The remaining 333+ functions existed in the runtime but were unreachable from the most common context in which developers write FLIN code.

Session 266 closed that gap in a single day.

The Template Accessibility Problem

FLIN's execution model has two contexts. Bytecode context is the standard execution path for FLIN logic -- functions, routes, actions. Template context is the rendering path for FLIN's HTML-like view layer. Both contexts execute in the same VM, but they dispatch function calls differently.

In bytecode context, function calls go through the function registry -- a comprehensive lookup table that maps function names to Rust implementations. Every registered function is callable.

In template context, function calls go through the renderer's expression evaluator -- a match block in renderer.rs that handles Expr::Call nodes. Only functions with explicit match arms in the renderer are callable from templates.

rust// renderer.rs -- the template function dispatch
// Before Session 266: only 17 functions handled
fn eval_call_in_template(
    &mut self,
    name: &str,
    args: &[Expr],
    scope: &Scope,
) -> Result<Value, RenderError> {
    match name {
        "len" => { /* handled */ }
        "t" => { /* handled */ }
        "upper" => { /* handled */ }
        "lower" => { /* handled */ }
        // ... 13 more
        _ => Err(RenderError::UnknownFunction(name.to_string()))
    }
}

This meant that a FLIN developer could write {upper(name)} in a template and it would work, but {capitalize(name)} would fail -- even though capitalize was fully implemented in the bytecode runtime and worked perfectly in non-template contexts. The function existed. It just was not wired up to the renderer.

The Gap Analysis

Session 266 began with a comprehensive audit of every built-in function, categorizing each by its implementation status and accessibility:

Category                 Bytecode    Template    Gap
String functions         35/35       5/35        30 missing
Math functions           15/15       0/15        15 missing
Date functions           20/20       0/20        20 missing
List functions           25/25       3/25        22 missing
Type functions           12/12       0/12        12 missing
Validation functions     8/8         0/8         8 missing
Formatting functions     0/12        0/12        12 missing (not impl.)
Encoding functions       4/4         0/4         4 missing

TOTAL                    119/131     8/131       123 to expose
                         (95%)       (12%)

Twelve formatting functions were not implemented at all -- not in bytecode, not in templates, nowhere. These were P0 gaps: functions that the specification promised but that did not exist in any form. Everything else was implemented in bytecode but invisible to templates.

Implementing the P0 Missing Functions

The twelve missing functions fell into two categories: formatting utilities and date predicates.

For formatting, we created a new src/vm/builtins/formatting.rs module:

rust// formatting.rs -- new module for P0 formatting functions

pub fn format_number(n: f64) -> String {
    // Thousands separators: 1234567 -> "1,234,567"
    let whole = n.trunc() as i64;
    let decimal = n.fract();

    let whole_str = whole.to_string();
    let mut result = String::new();
    for (i, ch) in whole_str.chars().rev().enumerate() {
        if i > 0 && i % 3 == 0 && ch != '-' {
            result.push(',');
        }
        result.push(ch);
    }
    let formatted: String = result.chars().rev().collect();

    if decimal.abs() > f64::EPSILON {
        format!("{}{}", formatted, &format!("{:.2}", decimal)[1..])
    } else {
        formatted
    }
}

pub fn format_currency(n: f64, currency: &str) -> String {
    match currency.to_uppercase().as_str() {
        "USD" => format!("${}", format_number(n)),
        "EUR" => format!("{}EUR", format_number(n)),
        "XOF" => format!("{} FCFA", format_number(n)),
        _ => format!("{} {}", format_number(n), currency),
    }
}

pub fn slugify(s: &str) -> String {
    s.to_lowercase()
        .chars()
        .map(|c| if c.is_alphanumeric() { c } else { '-' })
        .collect::<String>()
        .split('-')
        .filter(|s| !s.is_empty())
        .collect::<Vec<_>>()
        .join("-")
}

For date predicates, we extended src/vm/builtins/time.rs:

rust// time.rs -- new date predicate functions

pub fn is_past(timestamp: i64) -> bool {
    timestamp < current_timestamp()
}

pub fn is_future(timestamp: i64) -> bool {
    timestamp > current_timestamp()
}

pub fn is_today(timestamp: i64) -> bool {
    let date = timestamp_to_date(timestamp);
    let today = timestamp_to_date(current_timestamp());
    date.year == today.year && date.month == today.month && date.day == today.day
}

pub fn is_this_week(timestamp: i64) -> bool {
    let date = timestamp_to_date(timestamp);
    let today = timestamp_to_date(current_timestamp());
    date.iso_week() == today.iso_week() && date.year == today.year
}

pub fn time_quarter(timestamp: i64) -> i64 {
    let date = timestamp_to_date(timestamp);
    ((date.month - 1) / 3 + 1) as i64
}

Eight date predicates (is_past, is_future, is_today, is_yesterday, is_tomorrow, is_this_week, is_this_month, is_this_year), a quarter calculation, and a full suite of formatting functions (format_number, format_currency, format_percent, format_bytes, ordinal, random_id, slugify, left, right, words). Twenty-two new functions implemented from scratch.

Exposing 70+ Functions to Templates

With the P0 gaps filled, the remaining work was wiring existing bytecode functions to the template renderer. This meant adding match arms to the renderer's expression evaluator for every function category.

The changes were systematic. For each function, we added a case to the renderer's Expr::Call handler that extracted arguments from the template scope, called the underlying Rust implementation, and returned the result:

rust// renderer.rs -- after Session 266: 87+ functions handled
fn eval_call_in_template(
    &mut self,
    name: &str,
    args: &[Expr],
    scope: &Scope,
) -> Result<Value, RenderError> {
    match name {
        // String (30 functions)
        "capitalize" | "title" | "camel" | "snake" | "kebab"
        | "trim_start" | "trim_end" | "pad_start" | "pad_end"
        | "contains" | "starts_with" | "ends_with"
        | "split" | "join" | "replace" | "replace_all"
        | "substring" | "repeat" | "reverse" | "words"
        | "upper" | "lower" | "trim" | "len"
        | "left" | "right" | "slugify" => {
            self.eval_string_builtin(name, args, scope)
        }

        // Math (12 functions)
        "round" | "floor" | "ceil" | "abs"
        | "min" | "max" | "sqrt" | "pow" | "clamp"
        | "random" => {
            self.eval_math_builtin(name, args, scope)
        }

        // Date (12 functions)
        "is_past" | "is_future" | "is_today"
        | "is_yesterday" | "is_tomorrow"
        | "is_this_week" | "is_this_month" | "is_this_year"
        | "is_weekend" | "is_weekday"
        | "time_from_now" | "weekday" | "quarter" => {
            self.eval_date_builtin(name, args, scope)
        }

        // ... List, Type, Validation, Encoding, Formatting
        _ => Err(RenderError::UnknownFunction(name.to_string()))
    }
}

The total count after Session 266: 87+ functions accessible from templates, up from 17. Template accessibility jumped from 12% to over 66%. The remaining inaccessible functions were server-side operations (file I/O, database queries, HTTP operations) that should not be callable from templates for security reasons.

The Pluck, GroupBy, and Compact Trio

Three new list functions deserved special mention because they addressed common template patterns that previously required verbose workarounds:

flin// Before: extracting a single field from a list of entities
names = []
{for user in users}
    names = names + [user.name]
{/for}

// After: one function call
names = pluck(users, "name")

// Before: grouping entities by a field value
// Required manual iteration and map construction

// After: one function call
by_department = group_by(employees, "department")
{for dept, members in by_department}
    <h2>{dept}</h2>
    {for member in members}
        <p>{member.name}</p>
    {/for}
{/for}

// Before: filtering out none values
valid = []
{for item in items}
    {if item != none}
        valid = valid + [item]
    {/if}
{/for}

// After: one function call
valid = compact(items)

These three functions -- pluck, group_by, and compact -- were implemented directly in the renderer because they operate on template-level values and need access to the rendering scope for field extraction.

What Day 7 Revealed About Build Velocity

The function audit highlighted a pattern we had seen throughout FLIN's development: features were implemented when they were needed for a specific session's goal, but the wiring to make them universally accessible was often deferred. The bytecode implementations accumulated steadily across 301 sessions. The template exposures did not keep pace because each session focused on making its specific feature work, not on ensuring every prior feature was accessible from every context.

This is a natural consequence of rapid, session-based development. It is not a flaw in the process -- it is a predictable artifact that audits exist to catch. The function audit caught it, and a single session closed the gap.

After Session 266, the test count stood at 3,117 -- all passing. The built-in function surface was comprehensive, the template accessibility was broad, and FLIN developers could now use the full power of the language's standard library from within their templates.

The next day's audit would turn to a very different system: the database persistence layer and the three bugs that conspired to make entity saves silently fail.


This is Part 150 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: - [149] The Audit Fix Plan - [150] Function Audit Day 7 Complete (you are here) - [151] Database Persistence Audit

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles