Every programming language is shaped by its bugs. The features that survive are the ones that withstood the pressure of real usage. The design patterns that endure are the ones forged in debugging sessions at 2 AM. The documentation that matters most is written after something breaks.
Over the course of FLIN's development, from the first lexer token in January 2026 to the MVP binary release, we encountered hundreds of bugs. Most were routine -- off-by-one errors, missing semicolons, incorrect type signatures. But fifteen of them were different. These fifteen bugs did not just break features; they reshaped the language's architecture, influenced its design philosophy, and established principles that now govern every new feature we build.
This is the retrospective.
Bug 1: The CreateEntity Opcode That Went Missing
Session 270 -- Severity: Critical -- Time to fix: 30 minutes
The execute_until_return function was missing the CreateEntity opcode handler. Entity creation inside functions silently returned None. No error, no crash, no warning.
What it taught us: Parallel dispatch tables are a maintenance hazard. When two code paths must handle the same set of cases, they will drift out of sync. We now maintain a documented checklist of all 30+ opcodes that must exist in execute_until_return.
Design impact: We added validation that execute_until_return handles every opcode in the main dispatcher's entity category.
Bug 2: The For-Loop Stack Underflow
Sessions 061-062 -- Severity: High -- Time to fix: 2 sessions
Loop variables stored directly to locals were being cleaned up by end_scope() as if they were on the stack. Stack underflow crash on every for loop.
What it taught us: The compiler and VM must agree on the storage model for every variable category. Loop variables, function parameters, closure captures -- each has different lifecycle rules.
Design impact: The is_loop_var flag became a permanent part of FLIN's Local struct, establishing the principle that variable metadata must track storage semantics.
Bug 3: The Native Function Stack Leak
Session 062 -- Severity: High -- Time to fix: 3 lines of code
Native function calls did not clean up the function object from the stack. After fixing the stack underflow, for loops executed once and stopped because the leaked function object was mistaken for the iterator.
What it taught us: Stack discipline must be absolute. Every push must have a corresponding pop. Native and user-defined functions have different cleanup semantics, and both must be explicitly correct.
Design impact: We added stack size assertions to the test suite that verify stack balance after every operation.
Bug 4: None Property Access TypeError
Session 070 -- Severity: Critical -- Time to fix: 10 lines
Property access on None threw TypeError, blocking the entire temporal model. Every temporal query could produce None, making the feature unusable without verbose null checks.
What it taught us: In a language with None semantics, property access must propagate None instead of throwing. This is the "billion-dollar mistake" solved at the VM level.
Design impact: FLIN adopted implicit None propagation as a core language feature. None.property returns None, enabling clean temporal query chains.
Bug 5: HTML Whitespace Trimming
Session 074 -- Severity: Medium -- Time to fix: 2 hours
The lexer's .trim() on text nodes removed trailing spaces between static text and dynamic bindings. "Current: {name}" rendered as "Current:Alice" with no space.
What it taught us: Whitespace has two meanings in templates -- formatting (indentation) and content (inline spaces). A blanket transformation destroys the distinction.
Design impact: The smart_trim_text() function uses newline presence as a discriminator: multi-line text is trimmed, inline text is preserved verbatim.
Bug 6: Entity Version Not Synced After Save
Session 073 -- Severity: Critical -- Time to fix: 20 lines
After saving an entity, the VM's copy kept version=0 instead of the database's actual version. Temporal access computed wrong target versions and always returned None.
What it taught us: The database is the source of truth for all metadata. After any mutation, the VM must sync its representation with the database's version.
Design impact: The fetch-after-save pattern became mandatory for all entity operations. The VM always re-reads the entity from the database after saving to ensure metadata consistency.
Bug 7: Entity Schema Not Auto-Registered
Session 073 -- Severity: Critical -- Time to fix: 5 lines
The CreateEntity opcode constructed entities in the VM but never registered their schema with the database. The first save always failed with "UnknownEntityType."
What it taught us: In a language that emphasizes simplicity, boilerplate like schema declarations should be handled automatically.
Design impact: Schemas are now auto-registered on first entity creation. No explicit schema declaration is needed in FLIN code.
Bug 8: Bytecode Overwrites Injected State
Session 203 -- Severity: Critical -- Time to fix: Protected globals mechanism
The action handler injected browser state into the VM, but the page's initialization code overwrote it. newTodo = "" in the source erased the user's input.
What it taught us: State injection across execution boundaries needs protection mechanisms. The VM cannot assume it owns all global state.
Design impact: The protected_globals set prevents bytecode from overwriting injected values. This pattern is used whenever external state is injected into a VM instance.
Bug 9: Value::Text Not Handled by Trim
Session 203 -- Severity: High -- Time to fix: 2 lines per affected function
FLIN has two string representations: Value::Object (heap-allocated) and Value::Text (inline). The Trim opcode and extract_string() only handled Object, causing Text values to produce empty results.
What it taught us: Dual value representations are a constant source of bugs. Every string operation must handle both variants.
// The pattern that must appear everywhere
let s = match &value {
Value::Object(id) => self.get_string(*id)?.to_string(),
Value::Text(t) => t.clone(),
_ => return Err(/* ... */),
};Design impact: We established a coding standard requiring all string operations to handle both Value::Object and Value::Text.
Bug 10: The Theme Toggle Lexer Constraints
Sessions 245-251 -- Severity: Medium -- Time to fix: 5-7 sessions (2 lines of actual code)
The theme toggle required updating a DOM attribute, but FLIN's lexer did not support semicolons or single quotes in inline expressions. Five to seven sessions of attempted complex solutions before discovering assignment chaining with dot notation.
What it taught us: Sometimes language constraints guide you toward better solutions. The simplest fix -- using existing web APIs within the lexer's capabilities -- was superior to every compiler modification we attempted.
Design impact: We documented FLIN's expression constraints as features, not limitations. Assignment chaining (a = b = "value") became an officially supported pattern.
Bug 11: Entity Defaults Not Applied at Construction
Session 207 -- Severity: High -- Time to fix: Significant codegen change
Entity fields with defaults (done: bool = false) were not included in bytecode when the constructor omitted them. Missing fields became None, and !None = true caused toggles to always set true.
What it taught us: In a None-semantics language, explicit defaults are load-bearing. An uninitialized boolean field is not false -- it is None, which behaves differently under negation.
Design impact: The code generator now scans the entity schema for default values and includes them in the CreateEntity bytecode for any field not explicitly provided.
Bug 12: The Entity .get() vs .find() Type Mismatch
Session 205 -- Severity: Medium -- Time to fix: New opcode + type changes
The action handler used Todo.find(id) which returns Todo?, but functions expected Todo. The type checker correctly rejected the mismatch.
What it taught us: The distinction between "might exist" and "must exist" must be expressible in the type system.
Design impact: Added .get() method that returns the base type (not optional) and errors if the entity does not exist. Follows the universal ORM pattern.
Bug 13: HashMap Entity Ordering
Session 206 -- Severity: Medium -- Time to fix: Sort in all() method
Entities stored in a HashMap appeared in random order after page refresh. The todo list shuffled every time.
What it taught us: Data structures chosen for performance (HashMap for O(1) lookup) can violate user expectations about ordering. The persistence layer must impose a deterministic order.
Design impact: All entity query methods now sort results by (created_at, id), ensuring insertion-order stability.
Bug 14: Library Functions Without Type Annotations
Session 249 -- Severity: High -- Time to fix: Annotate all lib/ functions
Functions in lib/ files had untyped parameters, causing the type checker to assign ?T0 (unknown type) and reject all method calls on those parameters.
What it taught us: Type inference has boundaries. At module borders, types must be explicit. The type checker processes files independently and cannot infer parameter types from cross-file call sites.
Design impact: Established the rule that all function parameters in lib/ files must have explicit type annotations.
Bug 15: Layout Children Not Wrapped
Session 250 -- Severity: High -- Time to fix: Full-session implementation
The {children} placeholder in layouts was never replaced with page content. The rendering pipeline had no concept of layout wrapping.
What it taught us: Features that are "one concept to the developer" often touch every layer of the system. Layout wrapping required changes to the lexer, renderer, library API, HTTP server, and file watcher.
Design impact: Established the principle of end-to-end feature implementation. No feature is "done" until it works from source code through compilation, rendering, serving, and hot reload.
The Patterns That Emerged
Looking across all fifteen bugs, several meta-patterns emerge.
Pattern 1: The Silent Failure Spectrum
Seven of the fifteen bugs produced no error messages. The CreateEntity opcode, the for-loop leak, the version tracking, the state overwrite, the Value::Text handling, the entity defaults, and the layout wrapping all failed silently. This is not coincidence -- it reflects a fundamental challenge of building a language with None semantics.
When operations can legitimately return None, a None result is ambiguous: it might mean "no data" or "something broke." FLIN's response has been to add proactive detection mechanisms -- entity operations counters, WAL verification, stack size assertions -- that check for expected effects rather than waiting for errors.
Pattern 2: The Dual Representation Tax
Three bugs (Value::Text not handled, HashMap ordering, duplicate dispatch tables) were caused by maintaining two representations of the same thing. Two string types. Two storage structures. Two opcode dispatchers.
Dual representations exist for good reasons (performance, architecture). But each imposes an ongoing tax: every operation must handle both representations correctly. The pattern that emerged is rigorous: for every new operation, we now ask "does this handle both X and Y?" where X and Y are the dual representations.
Pattern 3: The Integration Gap
Five bugs (persistence chain, layout wrapping, action handler state, theme toggle, lib functions) were invisible in unit tests and only appeared during integration. Individual components worked correctly; the bugs lived in the connections between them.
This pattern led us to establish end-to-end test scenarios that exercise complete user workflows: create entity, save, reload page, verify persistence. These integration tests have caught more bugs than all our unit tests combined.
Pattern 4: The Simple Fix After Complex Investigation
Eight of the fifteen bugs had fixes smaller than 20 lines of code. The None handling bug was 10 lines. The for-loop fix was 3 lines. The theme toggle was 2 lines. The investigation to find each bug took hours or days; the fix itself took minutes.
This pattern reinforces the principle that debugging is primarily a search problem. The hard part is not writing the fix but knowing where to write it. Systematic instrumentation, execution tracing, and hypothesis testing are the tools that collapse the search space.
How Bugs Shape Language Design
Every language carries the scars of its development. FLIN's None propagation exists because of Bug 4. Its fetch-after-save pattern exists because of Bug 6. Its type-annotated library convention exists because of Bug 14. Its smart whitespace trimming exists because of Bug 5.
These are not theoretical design decisions made in the abstract. They are battle-tested principles forged in debugging sessions, validated by regression tests, and documented in session logs. They represent the wisdom that accumulates not from reading papers but from shipping software, finding the bugs, and fixing them.
The fifteen bugs in this arc span every layer of the system: lexer, parser, type checker, code generator, virtual machine, database, renderer, server, and client runtime. They touch every category of concern: correctness, performance, usability, and reliability. Together, they form a comprehensive map of where programming languages fail -- not in the happy path, but in the margins, the edge cases, and the integration points where components meet.
FLIN is a better language because of these bugs. Not despite them.
---
This is Part 170 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: - [169] The Embedding Model Choice Crisis - [170] 15 Bugs That Shaped the FLIN Language (you are here) - Next arc: FLIN Developer Experience and Tooling
This concludes Arc 15 -- Critical Bugs and War Stories. Fifteen articles covering the real bugs encountered during FLIN's development, the debugging processes that found them, and the design principles they established. Every bug made the language stronger.