Back to flin
flin

How the VM Executes Views

How FLIN's VM executes views: from bytecode opcodes to HTML rendering with reactive attribute binding.

Thales & Claude | March 25, 2026 11 min flin
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:

impl VirtualMachine {
    fn create_element(&mut self, tag: &str) {
        self.view_buffer.push_str(&format!("<{}", tag));
        self.element_stack.push(tag.to_string());
        self.in_open_tag = true;
    }

fn close_element(&mut self) { if self.in_open_tag { self.view_buffer.push('>'); self.in_open_tag = false; } if let Some(tag) = self.element_stack.pop() { self.view_buffer.push_str(&format!("", tag)); } }

fn set_attribute(&mut self, name: &str, value: Value) { let val_str = self.value_to_html_string(&value); self.view_buffer.push_str(&format!(" {}=\"{}\"", name, val_str)); } } ```

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:

CreateElement "div"        ; <div
SetAttribute "class"       ; class="counter"
                           ; >

CreateElement "h1" ;

AddText "Counter: " ; Counter: BindText (LoadGlobal count); {count} -- reactive binding CloseElement ;

CreateElement "button" ;

```

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);
}

Instruction::EndHandler => { self.end_handler(); }

Instruction::BindHandler => { self.bind_handler(); } ```

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:

view {
    {if count > 0}
        <p>Count is positive: {count}</p>
    {/if}
}

The compiler emits StartIf with a jump address to the else branch (or the end of the block):

LoadGlobal count    ; Push count
LoadInt0            ; Push 0
Gt                  ; count > 0?
StartIf [addr]      ; If false, jump to addr
  CreateElement "p"
  AddText "Count is positive: "
  BindText (count)
  CloseElement
EndIf

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