Skip to content

Latest commit

 

History

History
234 lines (180 loc) · 9.72 KB

File metadata and controls

234 lines (180 loc) · 9.72 KB

Bytecode reference

src/nsengine/program.ts defines the instruction set; src/nsengine/executor.ts defines its semantics.

Representation

interface Instruction {
    opcode: number;
    operand: any;      // number | string | boolean | JS object | null
    index?: number;    // compile-time only, stripped by finalize()
    label?: string;    // compile-time only
}

interface Program {
    engineVersion: string;
    nenv: Nenv;
    instructions: Instruction[];
}

Operands are untyped JavaScript values. OP_LOAD_EXTERNAL carries the host object itself; OP_LOAD_CONST_COPY carries a pre-built array/object/Set; OP_LOAD_LITERAL_OBJECT carries an IObjectGenerator. A Program is therefore a live JS structure, not a serialisable buffer — the WASM backend deals with this by interning non-numeric operands into a reference table (see wasm-engine.md).

Opcodes are assigned sequentially by an incrementing counter, so the declaration order in program.ts is the numeric order, and executor.ts's ops array must be kept in exactly the same order. OP_NAMES is a parallel array used by getOpName, printInstruction and printProgram.

Naming convention

OP_<NAME><dtype><size>_<location>

dtype   i int   f float   s string   b bool   o object
size    8       32        64
location o = instruction operand   s = stack

The 8/32/64 suffixes describe the intended width of a future typed value representation. In the current JS executor all three variants map to the same handler — OP_LOAD_LOCAL8, OP_LOAD_LOCAL32 and OP_LOAD_LOCAL64 are all op_load_local. The distinction is preserved because the compiler uses it to pick the right store/increment rewrite.

Stack notation

[ … ] → [ … ] shows the top of stack on the right.


Control flow

Opcode Operand Stack Effect
OP_NOOP [] → [] Advance. Emitted to flush a pending label.
OP_TERM Halt. The executor returns the top of stack, or 0 if empty.
OP_JUMP target index [] → [] ip = operand
OP_BRANCH_TRUE target index [v] → [] Jump if v truthy
OP_BRANCH_FALSE target index [v] → [] Jump if v falsy
OP_BRANCH_NULL target index [v] → [] Jump if v == null
OP_BRANCH_NOT_NULL target index [v] → [] Jump if v != null

Comparison branches (emitted by nothing today)

OP_BRANCH_EQUAL8/32/64, OP_BRANCH_NOT_EQUAL8/32/64, OP_BRANCH_GREATER_THAN8/32/64, OP_BRANCH_LESS_THAN8/32/64, OP_BRANCH_GREATER_THAN_OR_EQUAL8/32/64, OP_BRANCH_LESS_THAN_OR_EQUAL8/32/64

All pop two values and jump on the comparison. The compiler never emits them — it emits a comparison opcode followed by OP_BRANCH_FALSE. They exist as a peephole-optimisation target.

All three width variants share one handler. The handler pops b then a and tests a OP b, the same operand order as the non-branching comparisons, so a future peephole pass can substitute one for the other. testing/regressions.test.ts covers them with hand-built programs.


Loading constants

Opcode Operand Stack Effect
OP_LOAD_LITERAL_BOOL boolean [] → [b]
OP_LOAD_LITERAL_INT32 number [] → [n]
OP_LOAD_LITERAL_FLOAT64 number [] → [n]
OP_LOAD_LITERAL_NULL [] → [null]
OP_LOAD_LITERAL_STRING string [] → [s]
OP_LOAD_INSTRUCTION_REFERENCE "$name" [] → [addr] Pushes a code address. Normally rewritten in place to OP_CALL_INTERNAL.
OP_LOAD_PTR any JS value [] → [v] Pushes a pre-built constant by reference (no longer emitted)
OP_LOAD_CONST_COPY any JS value [] → [v] Pushes a deep copy of a pre-built constant. What folded collection literals compile to
OP_DUP [v] → [v v] Duplicates the top slot. Used by &&, || and ?.
OP_LOAD_LITERAL_LIST count n [v1…vn] → [array] Pops n, builds an Array
OP_LOAD_LITERAL_SET count n [v1…vn] → [Set] Pops n, builds a Set
OP_LOAD_LITERAL_OBJECT IObjectGenerator [v1…vn] → [object] Pops one value per declared property

Elements for list/set/object literals are compiled in reverse source order so that popping restores the correct order.


Arithmetic

Opcode Stack Effect
OP_ADDi / OP_ADDf / OP_ADDs [a b] → [a+b] All three run JS +
OP_SUBi / OP_SUBf [a b] → [a-b]
OP_MULi / OP_MULf [a b] → [a*b]
OP_DIVi / OP_DIVf [a b] → [a/b] Integer division is not truncated
OP_MODi / OP_MODf [a b] → [a%b]
OP_POWi / OP_POWf [a b] → [a**b]
OP_NEGi / OP_NEGf [a] → [-a]
OP_RANGEi32 / OP_RANGEf64 [start end] → [array] Inclusive of both ends; always steps by 1

OP_SB_REPLACE_s — operand: placeholder count n. Pops the template string, then pops n values, replacing ${0}${n-1} in turn.


Comparison and logic

Opcode Stack Effect
OP_GREATER_THANi/f [a b] → [a>b]
OP_LESS_THANi/f [a b] → [a<b]
OP_GREATER_THAN_OR_EQUALi/f [a b] → [a>=b]
OP_LESS_THAN_OR_EQUALi/f [a b] → [a<=b]
OP_EQUALi/f/b/s/o [a b] → [a==b] Loose ==
OP_NOT_EQUALi/f/b/s/o [a b] → [a!=b] Loose !=
OP_ANDb [a b] → [a&&b] Never emitted: && compiles to a branch
OP_ORb [a b] → [a||b] Never emitted: || compiles to a branch
OP_XORb [a b] → [a^b] Bitwise XOR on the JS values
OP_NOTb [a] → [!a]

Conversions

Opcode Effect
OP_INT_TO_FLOAT No-op (both are JS numbers)
OP_FLOAT_TO_INT Math.floor
OP_INT_TO_STRING .toString()
OP_FLOAT_TO_STRING .toString()

Variables

Opcode Operand Stack Effect
OP_LOAD_LOCAL8/32/64 slot offset [] → [v] push(stack[fp + operand]). Negative offsets address arguments.
OP_STORE_LOCAL8/32/64 slot offset [v] → [] stack[fp + operand] = pop()
OP_LOAD_MEMBER8/32/64 property name [obj] → [v] An absent member pushes null; a null object throws. Functions are .bind(obj)-ed.
OP_STORE_MEMBER8/32/64 property name [v obj] → [] obj[name] = v
OP_LOAD_ELEMENT8/32/64 index count [obj idx] → [v] Always one index; the compiler rejects a[i, j]
OP_STORE_ELEMENT8/32/64 index count [v obj idx] → []
OP_LOAD_EXTERNAL the host object [] → [obj]
OP_INCREMENT_LOCALi32/f64 slot offset [d] → [] stack[fp+op] += pop()
OP_DECREMENT_LOCALi32/f64 slot offset [d] → [] stack[fp+op] -= pop()
OP_INCREMENT_MEMBERi32/f64 property name [d obj] → [] obj[name] += d
OP_DECREMENT_MEMBERi32/f64 property name [d obj] → []
OP_INCREMENT_ELEMENTi32/f64 [d obj idx] → [] obj[idx] += d
OP_DECREMENT_ELEMENTi32/f64 [d obj idx] → []

The increment/decrement family leaves nothing on the stack, so x++ cannot be used as a sub-expression that yields a value. The compiler enforces this: using an assignment or increment in value position is a compile error.


Stack management

Opcode Operand Effect
OP_ALLOC_STACK count Push count × null
OP_POP_STACK count Pop count values
OP_ALLOC_HEAP count Push count × null onto heap (never emitted)
OP_POP_HEAP count Pop count from heap (never emitted)

Functions

Opcode Operand Effect
OP_CALL_INTERNAL target index push(fp), push(ip+1), fp = stack.length, ip = operand
OP_CALL_EXTERNAL arg count n Pops the callee; reads (does not pop) n args below it; result → ret
OP_CALL_STACK Like OP_CALL_INTERNAL but takes the target from the stack (never emitted)
OP_STACKPOP_VOID ret = null, unwind to fp, ip = pop(), fp = pop()
OP_STACKPOP_RET8/32/64 ret = pop(), unwind to fp, ip = pop(), fp = pop()
OP_PUSH_RETURN8/32/64 push(ret)

The three RET widths and the three PUSH_RETURN widths are identical handlers. The return value travels in the executor's ret register, not on the stack, which is why OP_PUSH_RETURN64 is a separate instruction emitted only when the caller wants the value.

OP_STACKPOP_VOID unwinds the frame's locals exactly like the RET variants, and clears ret, so a void call cannot resurrect the previous call's value.


Iteration

Opcode Operand Stack Effect
OP_WRAP_COLLECTION 1 [coll] → [iter] Wraps in a CollectionIterator
OP_INCREMENT_ITERATOR target index [iter] → [iter v] or [iter] → [] If exhausted: pop the iterator and jump to operand. Otherwise push the next value.

The iterator stays on the stack for the loop's whole lifetime, one slot below the loop body's working area. It is popped by OP_INCREMENT_ITERATOR when the collection runs out; a break or a multi-level continue that leaves the loop early emits its own OP_POP_STACK for each iterator it escapes.


Disassembly

import { printProgram, programToString, printInstruction } from './nsengine/program';

printProgram(program);            // console.table of opcode/operand
programToString(program);         // "[0]\tOP_LOAD_LITERAL_INT32 5\n…"

Compiling with { verboseMode: true } prints both the pre-link and final program plus the instruction reference table.