-
Notifications
You must be signed in to change notification settings - Fork 0
Bytecode Reference
Complete reference for the SQ# bytecode format, instruction set, stack VM architecture, and binary serialization format.
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"]
A BytecodeChunk is the compiled output from source code — the unit of execution for the VM.
| 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 |
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 2Supported constant types: Number, String, Boolean, Nil.
Global variable names and command names share a deduplicated table. Resolution priority at runtime:
- Check globals dictionary (user-defined global variables)
- Check command registry (nular commands matching the name)
chunk.AddGlobal("myGlobal"); // → index 0
chunk.AddGlobal("hint"); // → index 1 (could be global OR command)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 1Each 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) |
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 | 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 |
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.
| 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:
- Index into
_chunk.CommandIds[operand]to get the registered command ID - Look up in the appropriate delegate dictionary (
_nularCommands,_unaryCommands,_binaryCommands) - If found: invoke delegate → push result
- If not found: push
nil - 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.
| 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)| 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
| 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
| 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
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 --> [*]
| 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 |
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
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.
| 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 |
[length: 4 bytes LE, int32] [UTF-8 bytes...]
[count: 4 bytes LE] [ownerSchedulerId: 4 bytes LE] [isFrozen: 1 byte]
[element 0: tagged value]
[element 1: tagged value]
...
[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]
// 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);- 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
| 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 |
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
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();| Optimization | Description |
|---|---|
| Constant folding |
1 + 2 → PUSH_CONST 3 at compile time |
| String deduplication | Identical strings share one constant pool entry |
| Short-circuit compilation |
a && b → JUMP_IF_FALSE skipping b
|
| params inlining |
params ["_a", "_b"] → PUSH_LOCAL 0 + SELECT sequence |
| Switch optimization |
switch compiles to jump table for dense cases |
- Getting Started — architecture overview
- Host API — embedding and command registration
-
CLI Tool — compile to
.sqfcbinary format - Benchmarks — why interpreter is slower than Arma's JIT
- Getting Started
- Language Guide
- Type System
- Control Flow
- Functions & Code
- Strings & Text
- Arrays & Collections
- Concurrency
- Promise System
- Thread Safety
- Host API & Embedding
- CLI Tool
- Bytecode Reference
- Multiplayer
- Syntax Sugar
- Optimization Guide
- Benchmarks
- count
- select
- pushBack
- append
- deleteAt
- deleteRange
- resize
- reverse
- sort
- find
- in
- forEach
- freeze
- thaw
- isFrozen
- currentScheduler
- clientOwner
- allSchedulers
- schedulerName
- schedulerExists
- schedulerBudget
- setSchedulerBudget
- fiberCount
- readyFiberCount
- waitingFiberCount
- schedulerLoad
- sendTo