Boolean logic is supposed to be simple. True or false. On or off. Zero or one. But when None enters the picture, boolean logic develops a third state that can produce results that look correct on the surface while being fundamentally wrong underneath.
On January 17, 2026, the todo application's toggle checkbox had a persistent defect: it could mark a todo as "done" but could never unmark it. Clicking the checkbox always resulted in done = true, regardless of the previous state. The toggle was not toggling.
The Symptom
The debug output told a stark story:
Save: type=Todo, id=Some(1), fields={"done": Bool(true), "title": Text("Buy groceries")}
Save: type=Todo, id=Some(1), fields={"done": Bool(true), "title": Text("Buy groceries")}
Save: type=Todo, id=Some(1), fields={"done": Bool(true), "title": Text("Buy groceries")}Every toggle operation set done to true. The second toggle should have set it to false. The third should have set it back to true. Instead, true, true, true -- an infinite loop of done-ness.
The toggle function was straightforward:
flinfn toggleTodo(todo: Todo) {
todo.done = !todo.done
save todo
}The negation operator ! should flip true to false and false to true. This is first-semester computer science. What could possibly go wrong?
The Root Cause: Missing Defaults
The entity definition had a default value for the done field:
flinentity Todo {
title: text
done: bool = false
}The = false should mean that when a todo is created without specifying done, it defaults to false. But the creation code in addTodo only specified the title:
flinfn addTodo() {
todo = Todo { title: newTodo.trim() }
save todo
}The done field was not mentioned. In theory, the default value false should be applied. In practice, it was not.
The code generator's emit_entity_construct() function only emitted bytecode for fields that were explicitly provided. Missing fields with defaults were silently skipped:
rust// BEFORE: Only emits explicitly provided fields
fn emit_entity_construct(&mut self, name: &str, fields: &[(String, Expr)]) {
for (field_name, value) in fields {
self.emit_string(field_name);
self.emit_expr(value);
}
self.emit_u16_u8(OpCode::CreateEntity, type_idx, fields.len() as u8);
}When Todo { title: newTodo.trim() } was compiled, only the title field was emitted. The done field was not included in the bytecode. The CreateEntity opcode created an entity with one field (title), and the done field was simply absent.
Why None Breaks Negation
An absent field in FLIN evaluates to None. When the toggle function runs:
flintodo.done = !todo.doneIt evaluates !None. In FLIN's boolean semantics, None is falsy. The negation of a falsy value is true. So !None evaluates to true.
On the next toggle:
flintodo.done = !todo.done // !true = false... right?This should produce false. But there was a subtlety: the entity was being reloaded from the database between requests. And the database stored done: true after the first toggle. On reload, done was Bool(true), and !true correctly evaluates to false.
Wait -- so the second toggle should work? Let us trace more carefully.
The action handler creates a fresh VM for each request. It loads the entity from the database. At this point, todo.done is Bool(true). The toggle runs !true, which is false. The save persists done: false. This should be correct.
But the debug output showed true every time. The issue was that the entity was being re-created from the page source, not loaded from the database. The action handler compiled the full page source, which included:
flinentity Todo {
title: text
done: bool = false
}And then Todo.get(1) loaded the entity from the database with done: true. The toggle should have worked after the first incorrect save. We traced more carefully and found that the done field's default was not being applied on the first creation, making the first save set done: None (which was stored as "no done field"), and subsequent loads returned an entity without a done field at all. Each toggle was operating on a field that did not exist, producing !None = true every time.
The Fix
The fix modified emit_entity_construct() to apply default values for any entity field that was not explicitly provided:
rustfn emit_entity_construct(&mut self, name: &str, fields: &[(String, Expr)]) {
let provided_fields: HashSet<String> = fields.iter()
.map(|(n, _)| n.clone()).collect();
// Get defaults for missing fields from entity schema
let defaults_to_apply: Vec<(String, Expr)> = if let Some(schema) = self.entities.get(name) {
schema.defaults.iter()
.filter(|(field_name, _)| !provided_fields.contains(*field_name))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
} else {
Vec::new()
};
// Emit provided fields
for (field_name, value) in fields {
self.emit_string(field_name);
self.emit_expr(value);
}
// Emit default values for missing fields
for (field_name, default_expr) in &defaults_to_apply {
self.emit_string(field_name);
self.emit_expr(default_expr);
}
let total_fields = fields.len() + defaults_to_apply.len();
self.emit_u16_u8(OpCode::CreateEntity, type_idx, total_fields as u8);
}The key changes are:
- Build a set of explicitly provided field names
- Look up the entity schema to find fields with defaults
- Filter to find defaults for fields not explicitly provided
- Emit bytecode for both provided fields and default values
- Update the field count to include defaults
After the fix, Todo { title: newTodo.trim() } compiles to bytecode that includes both title (from the constructor) and done = false (from the default). The entity is created with done: Bool(false) from the start.
Verification
The behavior chain after the fix:
Create: Todo { title: "Buy groceries" }
-> done: Bool(false) (default applied)
Toggle 1: !false = true
-> done: Bool(true)
Toggle 2: !true = false
-> done: Bool(false)
Toggle 3: !false = true
-> done: Bool(true)Correct alternation. The toggle finally toggles.
Additional Fixes in the Same Session
Session 207 was a multi-fix session. Beyond the entity defaults bug, several other issues were resolved:
QueryGet Opcode Mapping
The QueryGet extended opcode (0x59), added in Session 205 for the .get() method, was missing from the from_byte() mapping. The opcode was defined and emitted but the VM could not decode it from bytecode. A one-line fix:
rust0x59 => Some(ExtendedOpCode::QueryGet),WAL created_at Preservation
After action handler operations, entity created_at timestamps were being overwritten instead of preserved. This broke ordering because entities that were toggled appeared to have been created at the toggle time rather than their original creation time.
Action Handler Response Code
The action handler returned HTTP 302 (redirect) after processing. This was correct for traditional form submissions but broke fetch() calls from JavaScript. The fix changed the response to HTTP 200 with a JSON body, letting the JavaScript code decide whether to reload.
JS Binding Scope Variables
The client-side runtime was attempting to bind loop variables from {for} blocks as reactive state. This caused errors because loop variables are transient -- they exist only during iteration and should not be tracked as bindable state.
The Default Value Principle
The entity defaults bug reveals a design principle: in a language with None semantics, explicit defaults are not optional -- they are load-bearing.
In languages without None (like Rust, where the compiler forces you to handle every case), an uninitialized field is a compilation error. The developer must provide a value. In languages with None (like JavaScript, Python, or FLIN), an uninitialized field silently becomes None. This is convenient for optional fields but dangerous for fields that have semantic defaults.
A done field defaulting to false is not a suggestion -- it is a requirement. A todo is not-done by default. If the default is not applied, the field is None, and None-as-not-done is a lie that breaks the first time someone tries to negate it.
The fix ensures that FLIN's entity defaults are more than documentation -- they are compiled into bytecode and applied automatically at construction time. When a developer writes done: bool = false, the language guarantees that done is false even if the constructor omits it.
This is Part 168 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: - [167] Entity Ordering and Time Format Bugs - [168] Entity Defaults and Toggle Fix (you are here) - [169] The Embedding Model Choice Crisis