Skip to content

Bytecode Reference

ffredyk edited this page Jul 24, 2026 · 3 revisions

Bytecode Reference

Complete reference for the SQ# bytecode format, instruction set, stack VM architecture, and binary serialization format.

Architecture Overview

SQ# uses a stack-based virtual machine with cooperative fiber scheduling. Each fiber has its own VM instance with an isolated stack, local variables, and instruction pointer.

flowchart LR
    subgraph SqVm["SqVm (per fiber)"]
        direction TB
        Stack["Stack (1024 elem)"]
        Locals["Locals _locals[0..N]"]
        Bytecode["Bytecode Chunk"]
        Handler["Handler Stack<br/>(try/catch)"]
        CmdReg["Command Registry<br/>(Nular/Unary/Binary)"]
    end
    SqVm --> State["State: Running | Yielded | Completed | Error"]
Loading

BytecodeChunk

A BytecodeChunk is the compiled output from source code — the unit of execution for the VM.

Structure

Field Type Description
Instructions List<Instruction> Bytecode instruction sequence
Constants List<SqValue> Constant pool (numbers, strings, booleans)
GlobalNames List<string> Global variable and command name table
CommandIds List<int> Registered command ID resolution table
LocalCount int Number of local variable slots
LocalNames List<string> Local variable names (debug/error messages)
Children List<BytecodeChunk> Nested code blocks ({ ... } and compile)
DebugInfo List<DebugInfoEntry> Source position mapping (instruction → line:col)
SourceFile string? Original source file path

Constant Pool

Constants are deduplicated — identical values share a single index:

// Compiler inserts constants:
chunk.AddConstant(new SqValue(42.0));        // → index 0
chunk.AddConstant(new SqValue(42.0));        // → index 0 (deduplicated)
chunk.AddConstant(new SqValue("hello"));     // → index 1
chunk.AddConstant(new SqValue(true));        // → index 2

Supported constant types: Number, String, Boolean, Nil.

Global Name Table

Global variable names and command names share a deduplicated table. Resolution priority at runtime:

  1. Check globals dictionary (user-defined global variables)
  2. Check command registry (nular commands matching the name)
chunk.AddGlobal("myGlobal");    // → index 0
chunk.AddGlobal("hint");        // → index 1 (could be global OR command)

Children (Nested Chunks)

Each code block { ... } in source becomes a child chunk. The MakeCode opcode references children by index:

_f = { _x * 2 };       // → child chunk at index 0
_g = { sleep 1; };     // → child chunk at index 1

Instruction Format

Each instruction is a 3-field struct:

Field Type Purpose
OpCode OpCode (byte) Operation to perform
Operand int Primary operand (index, offset, count)
Operand2 int Secondary operand (reserved, currently unused)

Emitting Instructions

chunk.Emit(OpCode.PushConst, 0);      // push constant at index 0
chunk.Emit(OpCode.PushLocal, 1);      // push local slot 1
chunk.Emit(OpCode.Jump, 15);          // jump to instruction 15

// Placeholder + patch pattern (for forward jumps):
int placeholder = chunk.EmitPlaceholder(OpCode.JumpIfFalse);
// ... emit body instructions ...
chunk.PatchJump(placeholder, afterBodyIndex);

OpCode Reference

Stack Manipulation

OpCode Mnemonic Operand Stack Effect Description
PushConst PUSH_CONST idx constant index → val Push constant from pool onto stack
PushLocal PUSH_LOCAL slot local slot → val Push local variable onto stack
StoreLocal STORE_LOCAL slot local slot val → Pop and store into local variable
PushGlobal PUSH_GLOBAL idx global name index → val Push global variable or nular command
StoreGlobal STORE_GLOBAL idx global name index val → Pop and store into global variable
Dup DUP a → a, a Duplicate top of stack
Pop POP a → Discard top of stack
Swap SWAP a, b → b, a Swap top two stack elements

Local Variable Semantics

nil assignment deletes the variable (SQF behavior):

StoreLocal slot:
  if value.IsNil:
    _localDefined[slot] = false   // variable deleted
    _locals[slot] = SqValue.Nil
  else:
    _localDefined[slot] = true
    _locals[slot] = value

Reading an undefined local throws SqUndefinedVariableError:

PushLocal slot:
  if !_localDefined[slot]:
    throw SqUndefinedVariableError
  push _locals[slot]

Global variables follow the same nil-deletion semantics via the globals dictionary.

Command Calls

OpCode Mnemonic Operand Stack Effect Description
NularCall NULAR_CALL cmdIdx command ID index → result Call nular command (no args)
UnaryCall UNARY_CALL cmdIdx command ID index arg → result Call unary command (1 arg, on right)
BinaryCall BINARY_CALL cmdIdx command ID index left, right → result Call binary command (2 args)

Command dispatch path:

  1. Index into _chunk.CommandIds[operand] to get the registered command ID
  2. Look up in the appropriate delegate dictionary (_nularCommands, _unaryCommands, _binaryCommands)
  3. If found: invoke delegate → push result
  4. If not found: push nil
  5. If delegate throws: push error value (wrapped exception)

VM built-in commands are registered in RegisterBuiltinCommands():

  • Arithmetic: +, -, *, /, %
  • Comparison: ==, !=, <, >, <=, >=
  • Logical: !
  • Array: pushBack, select, forEach, count
  • Shared: add, sub, get, set, compareSwap
  • Scheduler: clientOwner, currentScheduler, scheduler, isSchedulerLocal, canSuspend
  • Control: throw, isNil, spawnOn, await, timeout, terminate, scriptDone

Host-registered commands override or extend these.

Compound Value Construction

OpCode Mnemonic Operand Stack Effect Description
MakeArray MAKE_ARRAY count element count elem₁...elemₙ → array Pop N elements, create array
MakeHashMap MAKE_HASHMAP → hashmap Create empty hashmap
MakeCode MAKE_CODE childIdx child chunk index → code Create code value from child chunk
MakeShared MAKE_SHARED val → shared Pop value, create shared (atomic) wrapper

MakeArray preserves element order — first pushed element becomes array[0]:

Stack: [a, b, c]  →  MakeArray 3  →  Stack: [[a, b, c]]

MakeCode wraps a child BytecodeChunk into a SqCode value:

{ _x * 2 }    // compiler emits: MakeCode 0  (child chunk 0)

Control Flow

OpCode Mnemonic Operand Stack Effect Description
Jump JUMP targetIp target IP Unconditional jump
JumpIfFalse JUMP_IF_FALSE targetIp target IP cond → Pop, jump if falsy
JumpIfTrue JUMP_IF_TRUE targetIp target IP cond → Pop, jump if truthy
Call CALL argCount argument count [arg,] code → result Execute code block synchronously
Spawn SPAWN argCount argument count [arg,] code → handle Execute code block asynchronously
Ret RET Return from current chunk
Yield YIELD Yield fiber to scheduler

Truthiness rules for JumpIfFalse/JumpIfTrue:

  • nil → falsy
  • false → falsy
  • 0 (number) → falsy
  • Everything else → truthy

Call executes a code value synchronously:

Stack: [arg, codeVal]
1. Pop codeVal (must be SqType.Code)
2. Pop arg (if argCount > 0)
3. Create nested SqVm with code's BytecodeChunk
4. Set nestedVm._locals[0] = arg (the _this variable)
5. Execute nested VM to completion
6. Push result back onto current VM's stack

Spawn executes a code value asynchronously:

Stack: [arg, codeVal]
1. Pop codeVal
2. Pop arg (if argCount > 0)
3. Create nested BytecodeChunk
4. Call _scheduler.Spawn(childChunk, ...)
5. Push ScriptHandle (promise) onto stack

Error Handling

OpCode Mnemonic Operand Stack Effect Description
Throw THROW err → Pop error, jump to innermost catch
TryBegin TRY_BEGIN handlerIp catch handler IP Push handler IP onto handler stack
TryEnd TRY_END Pop handler (normal exit, no error)

try/catch compilation pattern:

TryBegin catchIp       // register handler
  [try body]
TryEnd                 // normal exit — remove handler
Jump afterCatch        // skip catch block
catchIp:               // handler target
  [catch body]
afterCatch:

Throw behavior:

Throw:
  if handlerStack is not empty:
    handlerIp = handlerStack.Pop()
    Push(error)         // _exception available in catch block
    _ip = handlerIp     // jump to catch handler
  else:
    _state = Error      // unhandled — terminate fiber

Introspection

OpCode Mnemonic Operand Stack Effect Description
IsNilLocal ISNIL_LOCAL slot local slot → bool Push true if local slot is undefined
IsNilGlobal ISNIL_GLOBAL idx global name index → bool Push true if global name is undefined

IsNilLocal: used for isNil _varName pattern:

IsNilLocal 3   → push true if _locals[3] is not defined

IsNilGlobal: used for isNil "varName" pattern:

IsNilGlobal 0  → push true if _chunk.GlobalNames[0] is undefined

VM State Machine

stateDiagram-v2
    [*] --> Running
    Running --> Yielded : yield / sleep / await
    Running --> Completed : ret / end of code
    Running --> Error : unhandled throw
    Yielded --> Running : auto-resume (handle done / sleep elapsed)
    Completed --> [*]
    Error --> [*]
Loading
State Description
Running Fiber is executing or ready to execute
Yielded Fiber suspended (sleep, await, yield)
Completed Fiber finished normally (Ret or end of code)
Error Fiber terminated with unhandled error

ExecuteStep Flow

Each call to ExecuteStep() runs one instruction:

ExecuteStep():
  if Yielded && reason resolved (e.g. await completed):
    → Running, push resolved value
  
  if IP past end:
    → Completed
  
  inst = Instructions[IP]
  IP++
  
  switch (inst.OpCode):
    PushConst / PushLocal / PushGlobal:  push value
    StoreLocal / StoreGlobal:            pop, store, delete on nil
    NularCall / UnaryCall / BinaryCall:  pop args, delegate invoke, push result
    MakeArray / MakeHashMap / MakeCode:  construct compound value
    MakeShared:                          wrap in atomic
    Jump / JumpIfFalse / JumpIfTrue:     control flow
    Call / Spawn:                        nested VM or fiber
    Ret:                                 complete
    Yield:                               suspend
    Dup / Pop / Swap:                    stack manipulation
    Throw / TryBegin / TryEnd:           error handling
    IsNilLocal / IsNilGlobal:            introspection
  
  return _state

Binary Serialization Format

SQ# supports serializing values and bytecode chunks to a compact binary format for game save/load, state snapshots, and network transfer. All multi-byte values are little-endian.

Value Tags

Tag Byte Type Payload
TagNil 0x00 Nothing None
TagBoolean 0x01 Boolean 1 byte (0 or 1)
TagNumber 0x02 Number 8 bytes (double, LE)
TagString 0x03 String Length-prefixed UTF-8
TagArray 0x04 Array Count (4B) + OwnerId (4B) + Frozen (1B) + elements
TagCode 0x05 Code Source string (length-prefixed)
TagHashMap 0x06 HashMap PairCount (4B) + OwnerId (4B) + key-value pairs
TagShared 0x07 Shared Current value as double (8B)
TagError 0x08 Error Message string
TagFrozenArray 0x09 FrozenArray Same as Array
TagNamespace 0x0A Namespace Variable count + key-value pairs
TagScriptHandle 0x0B ScriptHandle Resolved flag (1B) — host restores
TagScheduler 0x0C Scheduler Scheduler ID (4B)
≥ 0x80 Host type Custom Host type tag offset by 128 + string payload

String Encoding

[length: 4 bytes LE, int32] [UTF-8 bytes...]

Array Encoding

[count: 4 bytes LE] [ownerSchedulerId: 4 bytes LE] [isFrozen: 1 byte]
[element 0: tagged value]
[element 1: tagged value]
...

Bytecode Chunk Encoding

[instruction count: 4 bytes LE]
[instruction 0: OpCode (1B) + Operand (4B LE) + Operand2 (4B LE)]
...
[constant count: 4 bytes LE]
[constant 0: tagged value]
...
[global name count: 4 bytes LE]
[name 0: length-prefixed string]
...
[command ID count: 4 bytes LE]
[cmdId 0: 4 bytes LE]
...
[localCount: 4 bytes LE]
[child count: 4 bytes LE]
[child 0: nested BytecodeChunk]
...
[sourceFile: length-prefixed string]

API

// Serialize a value to byte array
byte[] data = SqBinarySerializer.Serialize(myValue);

// Deserialize a value from byte array
SqValue restored = SqBinarySerializer.Deserialize(data);

// Serialize entire bytecode chunk (for .sqfc files)
using var stream = File.Create("script.sqfc");
using var writer = new BinaryWriter(stream);
SqBinarySerializer.WriteChunk(writer, chunk);

// Deserialize bytecode chunk
using var stream = File.OpenRead("script.sqfc");
using var reader = new BinaryReader(stream);
BytecodeChunk chunk = SqBinarySerializer.ReadChunk(reader);

Limitations

  • Channels are not serializable (serialized as nil)
  • Script handles serialize as unresolved — host must restore
  • Host types serialize as opaque strings — host provides deserialization
  • Runtime objects (fibers, schedulers) not serialized — host responsibility

VM Limits

Limit Value Description
MaxStack 1024 Maximum stack depth
Stack element size 24 bytes SqValue struct (tag + double + object ref)
Local variable slots chunk.LocalCount Set by compiler based on private declarations
Instruction count int.MaxValue Practical limit much lower

Example: Compiled Code

Source:

private _x = 10;
private _y = _x * 3 + 5;

Bytecode:

PUSH_CONST 0          ; constant "10" (index 0)
STORE_LOCAL 1         ; → _x
PUSH_LOCAL 1          ; push _x
PUSH_CONST 1          ; constant "3" (index 1)  
BINARY_CALL 0         ; _x * 3  (command "*" at cmdIdx 0)
PUSH_CONST 2          ; constant "5" (index 2)
BINARY_CALL 1         ; + 5  (command "+" at cmdIdx 1)
STORE_LOCAL 2         ; → _y

Compiler Integration

The compiler (SQSharp.Compiler.Compiler) translates AST nodes to bytecode:

// Compile source to bytecode
var lexer = new Lexer(source);
var parser = new Parser(lexer);
var ast = parser.Parse();
var compiler = new Compiler();
BytecodeChunk chunk = compiler.Compile(ast);

// Execute
var vm = new SqVm(chunk);
SqValue result = vm.Execute();

Compiler Optimizations

Optimization Description
Constant folding 1 + 2PUSH_CONST 3 at compile time
String deduplication Identical strings share one constant pool entry
Short-circuit compilation a && bJUMP_IF_FALSE skipping b
params inlining params ["_a", "_b"]PUSH_LOCAL 0 + SELECT sequence
Switch optimization switch compiles to jump table for dense cases

See Also

SQ# Wiki

Home

Engine Docs

Migration

Commands

Value Constructors

Arithmetic

Comparison

Logic

Array

String

Math

Random

Type & Introspection

HashMap

Code Execution

Concurrency

Scheduler

Thread Safety

Error

Output

Time

Multiplayer

Compiler

Clone this wiki locally