Back to flin
flin

The Complete FLIN Opcode Reference

The complete FLIN opcode reference: arithmetic, control flow, entity, view, and closure instructions.

Thales & Claude | March 25, 2026 17 min flin
flinopcodesbytecodereferenceinstruction-setvm

Every programming language, at its core, is an instruction set. The syntax and semantics that developers interact with are a facade over a finite set of operations that the machine actually executes. This article documents every opcode in FLIN's bytecode -- the complete instruction set that powers the virtual machine.

FLIN's instruction set spans 16 categories and over 100 opcodes, ranging from basic stack manipulation to AI-powered semantic queries. Each opcode is a single byte, followed by zero or more operand bytes. This compact encoding keeps bytecode files small and instruction dispatch fast.

Understanding the opcode set is essential for anyone who wants to know what FLIN actually does when you write count++ or save user or {for todo in todos}. This is the language beneath the language.

---

Instruction Encoding

Before listing the opcodes, it helps to understand how they are encoded in the bytecode stream. FLIN uses variable-length instructions with five formats:

Format 0 -- No operands (1 byte):
  [opcode]
  Examples: Add, Sub, Mul, Pop, Dup, Return, Halt

Format 1 -- One u8 operand (2 bytes): [opcode] [u8] Examples: LoadLocal, StoreLocal (slots 0-255)

Format 2 -- One u16 operand (3 bytes): [opcode] [u16 little-endian] Examples: LoadConst, LoadGlobal, Jump

Format 3 -- One u32 operand (5 bytes): [opcode] [u32 little-endian] Examples: JumpFar, CallNative

Format 4 -- Two u8 operands (3 bytes): [opcode] [u8] [u8] Examples: Call (arity, const_idx) ```

All multi-byte values are little-endian. The most common instructions (arithmetic, stack ops) are Format 0 -- a single byte with no operands. This means typical FLIN bytecode averages about 1.5 bytes per instruction, which is remarkably compact.

---

Category 1: Control Flow (0x00-0x0F)

Control flow instructions manage the execution path -- halting, jumping, calling, and returning.

ByteMnemonicOperandsStack EffectDescription
0x00Halt----Stop execution, return top of stack
0x01Nop----No operation (used for padding)
0x02Jumpu16 addr--Unconditional jump to address
0x03JumpIfTrueu16 addrcond ->Pop and jump if truthy
0x04JumpIfFalseu16 addrcond ->Pop and jump if falsy
0x05JumpIfNoneu16 addrval ->Pop and jump if None
0x06Callu8 arityfn, args... -> resultCall function with N args
0x07CallNativeu16 idxargs... -> resultCall built-in function
0x08Return--result ->Return from function
0x09JumpFaru32 addr--Jump with 32-bit address

Halt is always the last instruction in a program. Without it, the VM would run past the end of the bytecode array. The compiler ensures every code path ends with either Halt or Return.

JumpIfNone exists specifically for optional value handling. In FLIN, many operations can return None (a database lookup that finds nothing, a list access that is out of bounds), and checking for None is common enough to deserve its own instruction rather than a two-instruction sequence of IsNone + JumpIfTrue.

---

Category 2: Stack Operations (0x10-0x1F)

Stack instructions manipulate the operand stack directly.

ByteMnemonicOperandsStack EffectDescription
0x10LoadConstu16 idx-> valuePush constant from pool
0x11Pop--value ->Discard top of stack
0x12Dup--v -> v, vDuplicate top
0x13Dup2--a, b -> a, b, a, bDuplicate top two
0x14Swap--a, b -> b, aSwap top two
0x15Over--a, b -> a, b, aCopy second to top
0x16LoadNone---> nonePush None
0x17LoadTrue---> truePush true
0x18LoadFalse---> falsePush false
0x19LoadInt0---> 0Push integer 0
0x1ALoadInt1---> 1Push integer 1
0x1BLoadIntN1---> -1Push integer -1
0x1CLoadSmallInti8 val-> valPush small int (-128..127)

The specialised constant instructions (LoadNone, LoadTrue, LoadFalse, LoadInt0, LoadInt1, LoadIntN1, LoadSmallInt) are optimisations. Instead of the three-byte LoadConst followed by a constant pool lookup, these single- or two-byte instructions push commonly used values directly. The compiler emits them automatically when it detects a constant that matches.

LoadSmallInt with an i8 operand covers integers from -128 to 127. This means loop counters, array indices, and most small numeric literals are encoded in just two bytes.

---

Category 3: Local Variables (0x20-0x2F)

// Examples of local variable instructions in use:
LoadLocal 0       // Push first local onto stack
StoreLocal 1      // Pop stack into second local
IncrLocal 0       // Increment first local in place
LoadLocalPush 2   // Push third local twice (load + dup)
ByteMnemonicOperandsStack EffectDescription
0x20LoadLocalu8 slot-> valueLoad local variable
0x21StoreLocalu8 slotvalue ->Store local variable
0x22LoadLocal16u16 slot-> valueLoad local (extended range)
0x23StoreLocal16u16 slotvalue ->Store local (extended range)
0x24IncrLocalu8 slot--Increment local by 1
0x25DecrLocalu8 slot--Decrement local by 1
0x26LoadLocalPushu8 slot-> val, valLoad and duplicate

IncrLocal and DecrLocal are fused instructions. The naive way to increment a local variable is LoadLocal 0; LoadInt1; Add; StoreLocal 0 -- four instructions, four dispatch cycles. IncrLocal 0 does the same thing in one instruction, two bytes. In a tight loop, this saves 75% of the overhead per iteration.

---

Category 4: Global Variables (0x30-0x3F)

ByteMnemonicOperandsStack EffectDescription
0x30LoadGlobalu16 name_idx-> valueLoad by name from constant pool
0x31StoreGlobalu16 name_idxvalue ->Store by name
0x32LoadGlobalDirectu16 slot-> valueLoad by slot (optimised)
0x33StoreGlobalDirectu16 slotvalue ->Store by slot (optimised)

LoadGlobalDirect and StoreGlobalDirect bypass the hash map lookup by using a numeric slot, assigned by the compiler when it can determine the global's index at compile time. This is an optimisation path -- the compiler falls back to name-based lookup when the global is defined dynamically.

---

Category 5: Arithmetic (0x40-0x4F)

ByteMnemonicOperandsStack EffectDescription
0x40Add--a, b -> resultAddition (also string concatenation)
0x41Sub--a, b -> resultSubtraction
0x42Mul--a, b -> resultMultiplication
0x43Div--a, b -> resultDivision (Float result)
0x44Mod--a, b -> resultModulo
0x45Neg--a -> resultNegation
0x46Incr--a -> resultIncrement by 1
0x47Decr--a -> resultDecrement by 1
0x48Pow--a, b -> resultExponentiation
0x49IntDiv--a, b -> resultInteger division

Add does double duty: for numbers, it performs addition. For strings, it performs concatenation. The VM checks the types of both operands at runtime and dispatches accordingly. This is one of the few places where FLIN's VM is dynamically typed -- the type checker has already verified that the operands are compatible, but the VM still needs to handle both cases.

Div always returns a Float, even for integer operands. IntDiv returns an Int, truncating the result. This matches the mathematical distinction between division (which can produce fractions) and integer division (which cannot).

---

Categories 6-7: Comparison and Logic (0x50-0x6F)

// Comparison chain example:
// if a < b and b < c
LoadLocal 0   // a
LoadLocal 1   // b
Lt            // a < b -> bool
LoadLocal 1   // b
LoadLocal 2   // c
Lt            // b < c -> bool
And           // (a < b) and (b < c) -> bool
ByteMnemonicStack EffectDescription
0x50Eqa, b -> boolEquality (deep comparison for objects)
0x51NotEqa, b -> boolInequality
0x52Lta, b -> boolLess than
0x53LtEqa, b -> boolLess than or equal
0x54Gta, b -> boolGreater than
0x55GtEqa, b -> boolGreater than or equal
0x56IsNonea -> boolTest for None
0x57IsNotNonea -> boolTest for not None
0x58Comparea, b -> intThree-way comparison (-1, 0, 1)
0x60Anda, b -> boolLogical AND
0x61Ora, b -> boolLogical OR
0x62Nota -> boolLogical NOT
0x63-0x68Bitwisea, b -> intBitAnd, BitOr, BitXor, BitNot, ShiftLeft, ShiftRight

A critical detail about Eq: it performs deep comparison for objects. Two strings with the same content but different ObjectId values are equal. This required a dedicated values_equal() helper in the VM that dereferences objects and compares their contents, rather than comparing their heap addresses. Getting this wrong was a bug we caught in Session 026 -- "hello" == "hello" was returning false because it was comparing ObjectId values.

---

Category 8: Objects and Fields (0x70-0x7F)

ByteMnemonicOperandsStack EffectDescription
0x70CreateObjectu8 countvals... -> objCreate object with N fields
0x71GetFieldu16 name_idxobj -> valueGet field by name
0x72SetFieldu16 name_idxobj, val ->Set field by name
0x73HasFieldu16 name_idxobj -> boolCheck field existence
0x74DeleteFieldu16 name_idxobj ->Remove field
0x75GetFieldDyn--obj, name -> valDynamic field access
0x76SetFieldDyn--obj, name, val ->Dynamic field set
0x77CreateEntityu16 type_idx, u8 countvals... -> entityCreate entity instance

GetFieldDyn and SetFieldDyn are for computed property access -- when the field name is not known at compile time (e.g., obj[fieldName]). The field name is popped from the stack as a string value.

CreateEntity is the bridge between objects and FLIN's data persistence layer. It creates an EntityInstance with a type tag, an auto-generated ID, and the specified fields. The entity is allocated on the heap and can be saved to FlinDB with the Save instruction.

---

Category 9: Lists and Maps (0x80-0x8F)

ByteMnemonicOperandsStack EffectDescription
0x80CreateListu16 countitems... -> listCreate list from stack values
0x81GetIndex--list, idx -> valueIndex into list
0x82SetIndex--list, idx, val ->Set list element
0x83ListLen--list -> intList length
0x84ListPush--list, val ->Append to list
0x85ListPop--list -> valRemove and return last
0x86ListConcat--a, b -> listConcatenate two lists
0x87ListSlice--list, start, end -> listSub-list extraction
0x88CreateMapu16 countk, v... -> mapCreate map from key-value pairs
0x89MapGet--map, key -> valueGet map value
0x8AMapSet--map, key, val ->Set map entry
0x8BMapHas--map, key -> boolKey existence check
0x8CMapDelete--map, key ->Remove entry
0x8DMapKeys--map -> listAll keys as list
0x8EMapValues--map -> listAll values as list

CreateList pops count values from the stack (in reverse order) and creates a list. CreateMap pops count * 2 values (alternating keys and values). Both allocate on the heap and push a Value::Object(ObjectId) onto the stack.

---

Category 10: Entity Operations (0x90-0x9F)

These opcodes interact with FlinDB, FLIN's built-in database:

ByteMnemonicOperandsStack EffectDescription
0x90Save--entity ->Persist entity to FlinDB
0x91Delete--entity ->Remove entity from FlinDB
0x92QueryAllu16 type_idx-> listGet all entities of a type
0x93QueryFindu16 type_idxid -> entity?Find by ID
0x94QueryWhereu16 type_idxpredicate -> listFilter entities
0x95QueryFirstu16 type_idx-> entity?First entity of type
0x96QueryCountu16 type_idx-> intCount entities
0x97QueryOrderu16 type, u16 fielddir -> listOrdered query
0x98EntityId--entity -> idExtract entity ID
0x99EntityType--entity -> stringExtract entity type name

In Session 011, Save and QueryAll were implemented against an in-memory HashMap. The real FlinDB storage engine (ZEROCORE) would come later, but the opcodes and their stack effects were fixed from the start.

---

Category 11: View Operations (0xA0-0xAF)

Covered in detail in the previous article. Sixteen instructions for creating elements, binding text and attributes, handling events, and conditional/loop rendering.

---

Categories 12-13: Intent and Temporal (0xB0-0xCF)

Intent operations power FLIN's AI capabilities:

ByteMnemonicStack EffectDescription
0xB0Askquery -> listNatural language database query
0xB1Searchquery, limit -> listSemantic search in entity fields
0xB2SearchMultiquery, limit, fields... -> listMulti-field semantic search
0xB3Embedtext -> embeddingGenerate vector embedding

Temporal operations power FLIN's time-travel queries:

ByteMnemonicStack EffectDescription
0xC0AtVersionentity, version -> entityAccess a specific version
0xC1AtTimeentity -> entityAccess entity at a time reference
0xC2AtDateentity, date -> entityAccess entity at a specific date
0xC3Historyentity -> listGet all versions
0xC4-0xC7Time loads-> timeLoadNow, LoadToday, LoadYesterday, ChangesSince

---

Category 14: Built-in Functions (0xD0-0xEF)

// Built-in function opcodes:
Print     = 0xD0,  // Debug print
ToString  = 0xD1,  // Convert to text
ToInt     = 0xD2,  // Convert to integer
ToFloat   = 0xD3,  // Convert to float
TypeOf    = 0xD4,  // Get type name string
Len       = 0xD5,  // Length of string/list/map

// Extended built-ins (0xE0-0xEF): Replace = 0xE0, // String replace Abs = 0xE1, // Math absolute value Floor = 0xE2, // Math floor Ceil = 0xE3, // Math ceiling Round = 0xE4, // Math round Min = 0xE5, // Math min Max = 0xE6, // Math max Sqrt = 0xE7, // Square root PowFn = 0xE8, // Power function ListFirst = 0xE9, // First list element ListLast = 0xEA, // Last list element ListReverse = 0xEB, // Reverse list ListSort = 0xEC, // Sort list ListMap = 0xED, // Higher-order map ListFilter = 0xEE, // Higher-order filter ListReduce = 0xEF, // Higher-order reduce ```

The last three (ListMap, ListFilter, ListReduce) are the HOF opcodes that trigger the continuation-based iteration machinery described in the closures article.

---

The Opcode Space

FLIN's opcode space is a single byte (0x00-0xFF), divided into 16 ranges of 16 opcodes each. Currently, about 110 of the 256 possible opcodes are assigned. This leaves room for approximately 146 future instructions without breaking the single-byte encoding.

The 0xF0-0xFF range is reserved for debug and special instructions:

ByteMnemonicDescription
0xF0BreakpointDebugger breakpoint
0xF1SourceLocSource location marker (for stack traces)
0xFEExtendedExtended opcode prefix (for future 2-byte opcodes)
0xFFInvalidInvalid instruction (always triggers an error)

The Extended prefix (0xFE) is a forward-looking design. If FLIN ever exhausts the single-byte opcode space, Extended followed by a second byte gives access to 256 additional opcodes without changing the encoding of existing instructions.

---

Design Philosophy

Three principles guided the opcode design:

Orthogonality. Each opcode does one thing. Add adds. LoadLocal loads a local. CreateElement creates an element. There are no instructions that combine unrelated operations (with the deliberate exception of fused instructions like IncrLocal, which combine a load, an add, and a store for performance).

Stack discipline. Every opcode documents its stack effect: how many values it consumes and how many it produces. The compiler uses these stack effects to verify that the generated bytecode is balanced -- every code path leaves the stack at the same height. A bug in the compiler that produces unbalanced bytecode would be caught by this verification.

Extension without breakage. The opcode ranges are grouped by category, with gaps left for future additions. Entity operations have room for more query types. View operations have room for more DOM manipulation instructions. The reserved ranges ensure that FLIN's bytecode format can evolve without invalidating existing compiled files.

---

This is Part 25 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: [26] Hot Module Reload in 42ms -- how file changes go from disk to browser in under 50 milliseconds.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles