How FLIN's VM executes views: from bytecode opcodes to HTML rendering with reactive attribute binding.
Thales & Claude| March 25, 2026 11 minflin
flinviewsrenderinghtmldomvmreactive
Most programming languages treat HTML rendering as an afterthought -- a string concatenation problem, or a template engine bolted on from the outside. FLIN treats views as bytecode. The same virtual machine that executes count = count + 1 also executes . View instructions are opcodes, just like arithmetic and control flow.
This design decision -- embedding view rendering into the instruction set -- is what makes FLIN a full-stack language rather than a backend language with a frontend library. There is no separate template compiler, no virtual DOM library, no JSX transform. The FLIN compiler parses view syntax and emits view opcodes. The VM executes them. HTML comes out the other end.
This article covers how that pipeline works: the view opcodes, the rendering buffer, attribute binding, event handling, and the bridge between bytecode execution and the DOM.
---
View Instructions: The Opcode Set
FLIN's bytecode specification reserves the 0xA0-0xAF range for view operations. There are 16 view opcodes:
0xA0 CreateElement tag_idx Create a new DOM element
0xA1 CloseElement -- Close the current element
0xA2 SetAttribute name_idx Set a static attribute
0xA3 BindText -- Bind reactive text content
0xA4 BindAttr name_idx Bind a reactive attribute
0xA5 CreateHandler event_idx Start an event handler block
0xA6 EndHandler -- End an event handler block
0xA7 BindHandler -- Attach handler to current element
0xA8 TriggerUpdate -- Trigger a reactivity update
0xA9 StartIf else_addr Begin a conditional view block
0xAA EndIf -- End a conditional view block
0xAB StartFor end_addr Begin a loop view block
0xAC NextFor var_slot Advance to next iteration
0xAD EndFor -- End a loop view block
0xAE AddText str_idx Add static text content
0xAF SelfClose -- Self-closing element
These opcodes turn the VM into an HTML generator. When the VM encounters CreateElement, it opens a new HTML tag. When it encounters CloseElement, it closes the tag. Everything between those two instructions becomes the element's content -- attributes, text, child elements, event handlers.
---
The Rendering Buffer
The VM maintains an internal rendering buffer -- a tree structure that accumulates HTML as view instructions execute:
The element_stack tracks nesting. When you create a
, the string "div" is pushed onto the element stack. When you close it, "div" is popped and the closing tag
is emitted. If the stack is empty when CloseElement executes, the VM reports a mismatched tag error.
This is a streaming renderer. It does not build a tree in memory and then serialise it. It writes HTML directly as instructions execute. This means the output is ready the moment execution completes, with no serialisation pass.
---
From FLIN Source to HTML
Consider this FLIN view:
count = 0
view {
Counter: {count}
}
```
The compiler translates this into a sequence of view opcodes:
The BindText instruction is different from AddText. AddText emits static text that never changes. BindText wraps the text in a reactive binding -- a with a data-flin-bind attribute that the client-side runtime can update when state changes:
<span data-flin-bind="count">0</span>
When the user clicks the button and count changes from 0 to 1, the client-side JavaScript finds this span and updates its text content. No virtual DOM diff. No full page re-render. Just a targeted text node update.
---
Event Handlers
Event handlers in FLIN are blocks of code attached to DOM events. The compiler emits them as a sequence of instructions between CreateHandler and EndHandler:
Instruction::CreateHandler(event_idx) => {
let event = self.get_identifier(event_idx);
self.start_handler(&event);
}
The handler body is FLIN bytecode, but it cannot be executed directly by the server-side VM -- it needs to run in the browser when the user interacts with the page. So the VM translates the handler body into JavaScript.
For a simple handler like count++, the translation is:
onclick="count++; _flinUpdate()"
The _flinUpdate() call at the end triggers the reactivity system to re-evaluate all bindings. This ensures that when count changes, every {count} interpolation in the view is updated.
For more complex handlers that involve function calls or conditional logic, the translation follows the same pattern: each FLIN operation maps to its JavaScript equivalent, and _flinUpdate() is appended at the end.
---
Conditional Views
FLIN supports conditional rendering with {if} blocks:
On the server side, the VM evaluates the condition and either renders the block or skips it. The initial HTML reflects the initial state. On the client side, the reactivity runtime re-evaluates the condition whenever the relevant variables change and shows or hides the block accordingly.
---
Loop Views
The {for} directive iterates over a list and renders its body once per element:
todos = ["Write code", "Test code", "Ship code"]
view {
{for todo in todos}
{todo}
{/for}
}
```
The VM executes this by:
1. Evaluating the list expression (todos).
2. For each element, binding it to the loop variable (todo).
3. Executing the loop body (the view instructions between StartFor and EndFor).
4. Repeating until all elements are processed.
LoadGlobal todos ; Push the list
StartFor [end_addr] ; Begin iteration
NextFor 0 ; Bind current element to local slot 0
CreateElement "li"
BindText (local 0) ; {todo}
CloseElement
EndFor
The NextFor instruction checks whether there are more elements. If yes, it binds the current element to the specified local variable slot and continues. If no, it jumps to end_addr to exit the loop.
---
The Client-Side Runtime
The server-side VM produces HTML with reactive annotations. The client-side JavaScript runtime makes those annotations live. The runtime is injected as a block at the end of the rendered HTML:
1. Creates a reactive proxy around the application state. Any write to a state variable triggers _flinUpdate via requestAnimationFrame.
2. Exposes state as global variables using Object.defineProperty. This allows event handlers to reference state variables directly (count++) without a prefix.
3. Updates reactive bindings by finding all elements with data-flin-bind attributes and re-evaluating their expressions.
The requestAnimationFrame batching is important. If a handler updates three variables, the DOM is only updated once -- at the next animation frame. Without batching, each variable update would trigger a separate DOM traversal.
---
The Bridge Between Bytecode and DOM
The view system represents a bridge between two worlds: the Rust VM that executes bytecode, and the browser that renders HTML and runs JavaScript. The bridge has three components:
1. Compile time: The FLIN compiler parses view syntax and emits view opcodes into the bytecode stream. No separate template compilation step.
2. Server time: The VM executes view opcodes and produces an HTML string with reactive annotations (data-flin-bind attributes) and a JavaScript runtime.
3. Client time: The browser renders the HTML. The JavaScript runtime handles interactivity, re-evaluating bindings when state changes.
This architecture means that FLIN views work without JavaScript for the initial render. The server produces complete HTML. A search engine crawler sees fully rendered content. A user on a slow connection sees the page immediately, before any JavaScript loads.
JavaScript is only needed for interactivity -- clicking buttons, updating counters, toggling visibility. And even then, the JavaScript is minimal: a reactive proxy, a global variable bridge, and a DOM update function. No framework. No virtual DOM. No hydration step.
---
Error Handling in Views
View rendering can fail in several ways: a mismatched tag, a missing variable in an interpolation, a type error in an attribute binding. The VM handles these gracefully:
Mismatched tags: If CloseElement finds the element stack empty, or the top of the stack does not match the expected tag, the VM produces a RuntimeError::ViewError with the mismatched tag names.
Missing variables: If a BindText instruction references a global that does not exist, the VM renders "" (empty string) rather than crashing. This allows views to render even when some state has not been initialised yet.
Type errors: If a SetAttribute receives a value that cannot be converted to a string (unlikely, since all FLIN values have a string representation), the VM uses a fallback string.
When the dev server encounters a compilation or rendering error, it does not show a blank page. It generates a styled error page in the browser with the error message, the source file name, and the line number. This inline error display means the developer never has to switch to the terminal to find out what went wrong.
---
Performance Considerations
The view rendering path is optimised for the common case: mostly static HTML with a few reactive bindings.
Static attributes (class="counter") are emitted directly as HTML strings. No runtime overhead. No reactive tracking. No JavaScript involvement.
Reactive bindings ({count}) are wrapped in annotated spans. The JavaScript runtime only traverses elements with data-flin-bind attributes, ignoring the vast majority of the DOM.
Event handlers are inlined as HTML attributes (onclick="..."). There is no event delegation, no synthetic event system, no event object wrapping. The browser's native event handling is fast enough, and avoiding abstraction layers keeps the client-side code tiny.
The total JavaScript runtime is under 50 lines. It loads in milliseconds and adds negligible overhead to page interactions.
---
The Compile-Render-Serve Pipeline
To see the full view rendering pipeline in action, trace the path from source to browser:
1. Compile: The FLIN compiler reads the .flin file, lexes it, parses it (including view blocks), type-checks it, and emits bytecode with view opcodes interspersed among regular opcodes.
2. Execute: The VM runs the bytecode. When it hits view opcodes, it builds HTML in the rendering buffer while simultaneously evaluating expressions (for reactive bindings) and translating event handlers (for JavaScript output).
3. Wrap: The rendered HTML is wrapped in a full HTML document: , with metadata, with the rendered content, and a block containing the reactive runtime.
4. Serve: The HTTP server returns this complete HTML document in response to a browser request.
5. Hydrate: The browser renders the static HTML immediately. The reactive runtime attaches event listeners and sets up the reactive proxy. From this point, the page is interactive.
This pipeline executes in under 20 milliseconds for a typical FLIN application. The result is a fully interactive web page generated from a single source file, with no build step, no bundling, no external dependencies.
---
What View Opcodes Made Possible
With view opcodes in the VM, FLIN became a full-stack language. A single .flin file could define data (entities), logic (functions), and presentation (views). The developer writes one file, and FLIN handles everything: compiling, rendering, serving, and updating.
No webpack. No Vite. No Babel. No React. No Svelte. No build pipeline with twelve configuration files. Just flin dev app.flin, and a working web application appears in the browser.
That is the promise of FLIN's view system. And it all starts with sixteen opcodes in a Rust VM.
---
This is Part 24 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO built a programming language from scratch.
Next up: [25] The Complete FLIN Opcode Reference -- every instruction in FLIN's bytecode, documented with stack effects and examples.