diff --git a/SPEC.md b/SPEC.md index 43f5149..d1a326c 100644 --- a/SPEC.md +++ b/SPEC.md @@ -478,6 +478,35 @@ type TypeScriptFunction = (...args: Value[]) => Promise | Value; **Effect**: Stop execution **Stack**: No change +## Label Syntax + +The bytecode format supports labels for improved readability: + +**Label Definition**: `.label_name:` marks an instruction position +**Label Reference**: `.label_name` in operands (e.g., `JUMP .loop_start`) + +Labels are resolved to numeric offsets during parsing. The original numeric offset syntax (`#N`) is still supported for backwards compatibility. + +Example with labels: +``` +JUMP .skip +.middle: + PUSH 999 + HALT +.skip: + PUSH 42 + HALT +``` + +Equivalent with numeric offsets: +``` +JUMP #2 +PUSH 999 +HALT +PUSH 42 +HALT +``` + ## Common Bytecode Patterns ### If-Else Statement @@ -485,59 +514,61 @@ type TypeScriptFunction = (...args: Value[]) => Promise | Value; LOAD 'x' PUSH 5 GT -JUMP_IF_FALSE 2 # skip then block, jump to else - # then block (N instructions) - JUMP M # skip else block +JUMP_IF_FALSE .else + # then block + JUMP .end +.else: # else block +.end: ``` ### While Loop ``` -loop_start: +.loop_start: # condition - JUMP_IF_FALSE N # jump past loop body - # body (N-1 instructions) - JUMP -N # jump back to loop_start -loop_end: + JUMP_IF_FALSE .loop_end + # body + JUMP .loop_start +.loop_end: ``` ### Function Definition ``` -MAKE_FUNCTION +MAKE_FUNCTION .function_body STORE 'functionName' -JUMP N # skip function body -function_body: - # function code (N instructions) +JUMP .skip_body +.function_body: + # function code RETURN -skip_body: +.skip_body: ``` ### Try-Catch ``` -PUSH_TRY #3 ; Jump to catch block (3 instructions ahead) +PUSH_TRY .catch ; try block POP_TRY -JUMP #2 ; Jump past catch block -; catch: +JUMP .end +.catch: STORE 'errorVar' ; Error is on stack ; catch block -; end: +.end: ``` ### Try-Catch-Finally ``` -PUSH_TRY #4 ; Jump to catch block (4 instructions ahead) -PUSH_FINALLY #7 ; Jump to finally block (7 instructions ahead) +PUSH_TRY .catch +PUSH_FINALLY .finally ; try block POP_TRY -JUMP #5 ; Jump to finally -; catch: +JUMP .finally +.catch: STORE 'errorVar' ; Error is on stack ; catch block - JUMP #2 ; Jump to finally -; finally: + JUMP .finally +.finally: ; finally block (executes in both cases) -; end: +.end: ``` ### Named Function Call @@ -553,17 +584,17 @@ CALL ### Tail Recursive Function ``` -MAKE_FUNCTION +MAKE_FUNCTION (n acc) .factorial_body STORE 'factorial' -JUMP 10 # skip to main -factorial_body: +JUMP .main +.factorial_body: LOAD 'n' PUSH 0 EQ - JUMP_IF_FALSE 2 # skip to recurse + JUMP_IF_FALSE .recurse LOAD 'acc' RETURN -recurse: +.recurse: LOAD 'factorial' LOAD 'n' PUSH 1 @@ -574,7 +605,7 @@ recurse: PUSH 2 # positionalCount PUSH 0 # namedCount TAIL_CALL # No stack growth! -main: +.main: LOAD 'factorial' PUSH 5 PUSH 1 diff --git a/examples/README.md b/examples/README.md index fe5a880..c0d37ac 100644 --- a/examples/README.md +++ b/examples/README.md @@ -23,8 +23,8 @@ You can run any example using the reef binary: ### Advanced Function Features - **variadic.reef** - Variadic parameters that collect remaining positional arguments (`...rest`) -- **kwargs.reef** - Named arguments that collect unmatched named args into a dict (`@kwargs`) -- **mixed-variadic-kwargs.reef** - Combining variadic positional and named arguments +- **named.reef** - Named arguments that collect unmatched named args into a dict (`@named`) +- **mixed-variadic-named.reef** - Combining variadic positional and named arguments - **tail-recursion.reef** - Tail-recursive factorial using TAIL_CALL - **closure.reef** - Closures that capture variables from outer scope @@ -38,12 +38,16 @@ The `.reef` files use the following syntax: - Comments start with `;` - Instructions are written as `OPCODE operand` -- Labels use `#N` for relative offsets -- Function signatures: `MAKE_FUNCTION (params) #address` +- Labels: + - Definition: `.label_name:` marks an instruction position + - Reference: `.label_name` in operands (e.g., `JUMP .loop_start`) + - Also supports numeric offsets: `#N` for relative offsets +- Function signatures: `MAKE_FUNCTION (params) address` + - Address can be a label `.body` or numeric offset `#7` - Fixed params: `(a b c)` - Variadic: `(a ...rest)` - - Named args: `(a @kwargs)` - - Mixed: `(a ...rest @kwargs)` + - Named args: `(a @named)` + - Mixed: `(a ...rest @named)` ## Function Calling Convention @@ -57,12 +61,19 @@ When calling functions, the stack must contain (bottom to top): Example with positional arguments: ``` -LOAD functionName ; push function onto stack +MAKE_FUNCTION (a b) .add_body PUSH arg1 ; first positional arg PUSH arg2 ; second positional arg PUSH 2 ; positional count PUSH 0 ; named count CALL +HALT + +.add_body: + LOAD a + LOAD b + ADD + RETURN ``` Example with named arguments: diff --git a/examples/closure.reef b/examples/closure.reef index 2046d77..886646f 100644 --- a/examples/closure.reef +++ b/examples/closure.reef @@ -1,6 +1,6 @@ ; Closure example: counter function that captures state ; outer() returns inner() which increments and returns captured count -MAKE_FUNCTION () #10 +MAKE_FUNCTION () .outer_body PUSH 0 PUSH 0 CALL @@ -10,15 +10,17 @@ PUSH 0 PUSH 0 CALL HALT -; Outer function body -PUSH 0 -STORE count -MAKE_FUNCTION () #14 -RETURN -; Inner function body (closure over count) -LOAD count -PUSH 1 -ADD -STORE count -LOAD count -RETURN + +.outer_body: + PUSH 0 + STORE count + MAKE_FUNCTION () .inner_body + RETURN + +.inner_body: + LOAD count + PUSH 1 + ADD + STORE count + LOAD count + RETURN diff --git a/examples/exception-handling.reef b/examples/exception-handling.reef index a86bdc0..1a102a6 100644 --- a/examples/exception-handling.reef +++ b/examples/exception-handling.reef @@ -1,19 +1,23 @@ ; Try-catch-finally example -PUSH_TRY #9 -PUSH_FINALLY #16 +PUSH_TRY .catch +PUSH_FINALLY .finally PUSH 'Something went wrong!' THROW PUSH 999 POP_TRY -JUMP #4 -HALT -; Catch block -STORE err -PUSH 'Caught: ' -LOAD err -ADD -HALT -; Finally block -POP -PUSH 'Finally executed' +JUMP .after_catch HALT + +.catch: + STORE err + PUSH 'Caught: ' + LOAD err + ADD + HALT + +.finally: + POP + PUSH 'Finally executed' + HALT + +.after_catch: diff --git a/examples/loop.reef b/examples/loop.reef index 2985f37..b1ade91 100644 --- a/examples/loop.reef +++ b/examples/loop.reef @@ -1,17 +1,20 @@ ; Loop example: count from 0 to 5 PUSH 0 STORE i -; Loop condition -LOAD i -PUSH 5 -LT -JUMP_IF_FALSE #6 + +.loop_start: + LOAD i + PUSH 5 + LT + JUMP_IF_FALSE .loop_end + ; Loop body -LOAD i -PUSH 1 -ADD -STORE i -JUMP #-9 -; After loop -LOAD i -HALT + LOAD i + PUSH 1 + ADD + STORE i + JUMP .loop_start + +.loop_end: + LOAD i + HALT diff --git a/examples/mixed-variadic-named.reef b/examples/mixed-variadic-named.reef index 96463da..caea3f2 100644 --- a/examples/mixed-variadic-named.reef +++ b/examples/mixed-variadic-named.reef @@ -1,6 +1,6 @@ ; Mixed variadic and named arguments -; Function takes one fixed param, variadic positional, and kwargs -MAKE_FUNCTION (x ...rest @kwargs) #10 +; Function takes one fixed param, variadic positional, and named +MAKE_FUNCTION (x ...rest @named) .fn_body PUSH 1 PUSH 2 PUSH 3 @@ -10,9 +10,10 @@ PUSH 3 PUSH 1 CALL HALT -; Return array with all three parts -LOAD x -LOAD rest -LOAD kwargs -MAKE_ARRAY #3 -RETURN + +.fn_body: + LOAD x + LOAD rest + LOAD named + MAKE_ARRAY #3 + RETURN diff --git a/examples/named.reef b/examples/named.reef index f062574..3040662 100644 --- a/examples/named.reef +++ b/examples/named.reef @@ -1,5 +1,6 @@ -; Named arguments (kwargs) example -MAKE_FUNCTION (x @kwargs) #9 +; Named arguments (named) example +MAKE_FUNCTION (x @named) .fn_body +PUSH 100 PUSH 'name' PUSH 'Alice' PUSH 'age' @@ -8,6 +9,7 @@ PUSH 1 PUSH 2 CALL HALT -; Return the kwargs dict -LOAD kwargs -RETURN + +.fn_body: + LOAD named + RETURN diff --git a/examples/simple-function.reef b/examples/simple-function.reef index efd2c2c..6c973dd 100644 --- a/examples/simple-function.reef +++ b/examples/simple-function.reef @@ -1,12 +1,14 @@ ; Simple function that adds two numbers -MAKE_FUNCTION (a b) #7 +MAKE_FUNCTION (a b) .add_body PUSH 10 PUSH 20 PUSH 2 PUSH 0 CALL HALT -LOAD a -LOAD b -ADD -RETURN + +.add_body: + LOAD a + LOAD b + ADD + RETURN diff --git a/examples/tail-recursion.reef b/examples/tail-recursion.reef index caa7af1..0f480b9 100644 --- a/examples/tail-recursion.reef +++ b/examples/tail-recursion.reef @@ -1,6 +1,6 @@ ; Tail-recursive factorial function ; factorial(n, acc) = if n <= 1 then acc else factorial(n-1, n*acc) -MAKE_FUNCTION (n acc) #21 +MAKE_FUNCTION (n acc) .factorial_body DUP PUSH 5 PUSH 1 @@ -8,21 +8,23 @@ PUSH 2 PUSH 0 CALL HALT -; Function body -LOAD n -PUSH 1 -LTE -JUMP_IF_FALSE #2 -LOAD acc -RETURN -; Tail recursive call -DUP -LOAD n -PUSH 1 -SUB -LOAD n -LOAD acc -MUL -PUSH 2 -PUSH 0 -TAIL_CALL + +.factorial_body: + LOAD n + PUSH 1 + LTE + JUMP_IF_FALSE .recurse + LOAD acc + RETURN + +.recurse: + DUP + LOAD n + PUSH 1 + SUB + LOAD n + LOAD acc + MUL + PUSH 2 + PUSH 0 + TAIL_CALL diff --git a/examples/variadic.reef b/examples/variadic.reef index 7db8142..0c1c733 100644 --- a/examples/variadic.reef +++ b/examples/variadic.reef @@ -1,5 +1,5 @@ ; Variadic function that sums all arguments -MAKE_FUNCTION (x ...rest) #19 +MAKE_FUNCTION (x ...rest) .sum_body PUSH 5 PUSH 10 PUSH 15 @@ -8,26 +8,32 @@ PUSH 4 PUSH 0 CALL HALT -; Function body: sum x and all rest elements + +.sum_body: LOAD x STORE sum PUSH 0 STORE i -LOAD i -LOAD rest -ARRAY_LEN -LT -JUMP_IF_FALSE #8 -LOAD sum -LOAD rest -LOAD i -ARRAY_GET -ADD -STORE sum -LOAD i -PUSH 1 -ADD -STORE i -JUMP #-18 -LOAD sum -RETURN + +.loop_start: + LOAD i + LOAD rest + ARRAY_LEN + LT + JUMP_IF_FALSE .loop_end + + LOAD sum + LOAD rest + LOAD i + ARRAY_GET + ADD + STORE sum + LOAD i + PUSH 1 + ADD + STORE i + JUMP .loop_start + +.loop_end: + LOAD sum + RETURN diff --git a/src/bytecode.ts b/src/bytecode.ts index b754af7..2e6e200 100644 --- a/src/bytecode.ts +++ b/src/bytecode.ts @@ -19,6 +19,7 @@ export type Constant = // Parse bytecode from human-readable string format. // Operand types are determined by prefix/literal: // #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3) +// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body) // name -> variable/function name (e.g., LOAD x, CALL_NATIVE add) // 42 -> number constant (e.g., PUSH 42) // "str" -> string constant (e.g., PUSH "hello") @@ -27,8 +28,12 @@ export type Constant = // false -> boolean constant (e.g., PUSH false) // null -> null constant (e.g., PUSH null) // +// Labels: +// .label_name: -> label definition (marks current instruction position) +// // Function definitions: -// MAKE_FUNCTION (x y) #7 -> basic function +// MAKE_FUNCTION (x y) #7 -> basic function (numeric offset) +// MAKE_FUNCTION (x y) .body -> basic function (label reference) // MAKE_FUNCTION (x y=42) #7 -> with defaults // MAKE_FUNCTION (x ...rest) #7 -> variadic // MAKE_FUNCTION (x @named) #7 -> named @@ -97,10 +102,9 @@ function parseFunctionParams(paramStr: string, constants: Constant[]): { export function toBytecode(str: string): Bytecode /* throws */ { const lines = str.trim().split("\n") - const bytecode: Bytecode = { - instructions: [], - constants: [] - } + // First pass: collect labels and their positions + const labels = new Map() + const cleanLines: string[] = [] for (let line of lines) { // Strip semicolon comments @@ -112,6 +116,24 @@ export function toBytecode(str: string): Bytecode /* throws */ { const trimmed = line.trim() if (!trimmed) continue + // Check for label definition (.label_name:) + if (/^\.[a-zA-Z_][a-zA-Z0-9_]*:$/.test(trimmed)) { + const labelName = trimmed.slice(1, -1) + labels.set(labelName, cleanLines.length) + continue + } + + cleanLines.push(trimmed) + } + + // Second pass: parse instructions and resolve label references + const bytecode: Bytecode = { + instructions: [], + constants: [] + } + + for (let i = 0; i < cleanLines.length; i++) { + const trimmed = cleanLines[i]! const [op, ...rest] = trimmed.split(/\s+/) const opCode = OpCode[op as keyof typeof OpCode] @@ -126,15 +148,28 @@ export function toBytecode(str: string): Bytecode /* throws */ { // Special handling for MAKE_FUNCTION with paren syntax if (opCode === OpCode.MAKE_FUNCTION && operand.startsWith('(')) { - // Parse: MAKE_FUNCTION (params) #body - const match = operand.match(/^(\(.*?\))\s+(#-?\d+)$/) + // Parse: MAKE_FUNCTION (params) #body or MAKE_FUNCTION (params) .label + const match = operand.match(/^(\(.*?\))\s+(#-?\d+|\.[a-zA-Z_][a-zA-Z0-9_]*)$/) if (!match) { throw new Error(`Invalid MAKE_FUNCTION syntax: ${operand}`) } const paramStr = match[1]! const bodyStr = match[2]! - const body = parseInt(bodyStr.slice(1)) + + let body: number + if (bodyStr.startsWith('.')) { + // Label reference + const labelName = bodyStr.slice(1) + const labelPos = labels.get(labelName) + if (labelPos === undefined) { + throw new Error(`Undefined label: ${labelName}`) + } + body = labelPos + } else { + // Numeric offset + body = parseInt(bodyStr.slice(1)) + } const { params, defaults, variadic, named } = parseFunctionParams(paramStr, bytecode.constants) @@ -150,7 +185,22 @@ export function toBytecode(str: string): Bytecode /* throws */ { operandValue = bytecode.constants.length - 1 } - else if (operand.startsWith('#')) { + else if (operand.startsWith('.')) { + // Label reference - resolve to relative offset + const labelName = operand.slice(1) + const labelPos = labels.get(labelName) + if (labelPos === undefined) { + throw new Error(`Undefined label: ${labelName}`) + } + // For PUSH_TRY and PUSH_FINALLY, use absolute position + // For other jump instructions, use relative offset from next instruction (i + 1) + if (opCode === OpCode.PUSH_TRY || opCode === OpCode.PUSH_FINALLY) { + operandValue = labelPos + } else { + operandValue = labelPos - (i + 1) + } + + } else if (operand.startsWith('#')) { // immediate number operandValue = parseInt(operand.slice(1)) diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 0616df0..5b9ea3e 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -188,9 +188,10 @@ test("AND pattern - short circuits when false", async () => { PUSH 0 EQ DUP - JUMP_IF_FALSE #2 + JUMP_IF_FALSE .end POP PUSH 999 + .end: ` const result = await run(toBytecode(str)) expect(result.type).toBe('boolean') @@ -203,9 +204,10 @@ test("AND pattern - evaluates both when true", async () => { const str = ` PUSH 1 DUP - JUMP_IF_FALSE #2 + JUMP_IF_FALSE .end POP PUSH 2 + .end: ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) }) @@ -214,9 +216,10 @@ test("OR pattern - short circuits when true", async () => { const str = ` PUSH 1 DUP - JUMP_IF_TRUE #2 + JUMP_IF_TRUE .end POP PUSH 2 + .end: ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 1 }) }) @@ -227,9 +230,10 @@ test("OR pattern - evaluates second when false", async () => { PUSH 0 EQ DUP - JUMP_IF_TRUE #2 + JUMP_IF_TRUE .end POP PUSH 2 + .end: ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) }) @@ -263,16 +267,18 @@ test("isTruthy - only null and false are falsy", async () => { // 0 is truthy (unlike JS) const str1 = ` PUSH 0 - JUMP_IF_FALSE #1 + JUMP_IF_FALSE .end PUSH 1 + .end: ` expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 }) // empty string is truthy (unlike JS) const str2 = ` PUSH '' - JUMP_IF_FALSE #1 + JUMP_IF_FALSE .end PUSH 1 + .end: ` expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 1 }) @@ -281,8 +287,9 @@ test("isTruthy - only null and false are falsy", async () => { PUSH 0 PUSH 0 EQ - JUMP_IF_FALSE #1 + JUMP_IF_FALSE .end PUSH 999 + .end: ` expect(await run(toBytecode(str3))).toEqual({ type: 'number', value: 999 }) }) @@ -323,20 +330,22 @@ test("STORE and LOAD - multiple variables", async () => { test("JUMP - relative jump forward", async () => { const str = ` PUSH 1 - JUMP #1 + JUMP .skip PUSH 100 + .skip: PUSH 2 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) }) -test("JUMP - backward offset demonstrates relative jumps", async () => { +test("JUMP - forward jump skips instructions", async () => { // Use forward jump to skip, demonstrating relative addressing const str = ` PUSH 100 - JUMP #2 + JUMP .end PUSH 200 PUSH 300 + .end: PUSH 400 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 400 }) @@ -347,8 +356,9 @@ test("JUMP_IF_FALSE - conditional jump when false", async () => { PUSH 1 PUSH 0 EQ - JUMP_IF_FALSE #1 + JUMP_IF_FALSE .skip PUSH 100 + .skip: PUSH 42 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) @@ -357,8 +367,9 @@ test("JUMP_IF_FALSE - conditional jump when false", async () => { test("JUMP_IF_FALSE - no jump when true", async () => { const str = ` PUSH 1 - JUMP_IF_FALSE #1 + JUMP_IF_FALSE .skip PUSH 100 + .skip: ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 }) }) @@ -366,8 +377,9 @@ test("JUMP_IF_FALSE - no jump when true", async () => { test("JUMP_IF_TRUE - conditional jump when true", async () => { const str = ` PUSH 1 - JUMP_IF_TRUE #1 + JUMP_IF_TRUE .skip PUSH 100 + .skip: PUSH 42 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) @@ -521,11 +533,12 @@ test("BREAK - throws error when no break target", async () => { // BREAK requires a break target frame on the call stack // A single function call has no previous frame to mark as break target const bytecode = toBytecode(` - MAKE_FUNCTION () #5 + MAKE_FUNCTION () .fn PUSH 0 PUSH 0 CALL HALT + .fn: BREAK `) @@ -541,18 +554,20 @@ test("BREAK - exits from nested function call", async () => { // BREAK unwinds to the break target (the outer function's frame) // Main calls outer, outer calls inner, inner BREAKs back to outer's caller (main) const bytecode = toBytecode(` - MAKE_FUNCTION () #6 + MAKE_FUNCTION () .outer PUSH 0 PUSH 0 CALL PUSH 42 HALT - MAKE_FUNCTION () #11 + .outer: + MAKE_FUNCTION () .inner PUSH 0 PUSH 0 CALL PUSH 99 RETURN + .inner: BREAK `) @@ -566,17 +581,19 @@ test("JUMP backward - simple loop", async () => { const bytecode = toBytecode(` PUSH 0 STORE counter + .loop: LOAD counter PUSH 3 EQ - JUMP_IF_FALSE #2 + JUMP_IF_FALSE .body LOAD counter HALT + .body: LOAD counter PUSH 1 ADD STORE counter - JUMP #-11 + JUMP .loop `) const result = await run(bytecode) diff --git a/tests/bytecode.test.ts b/tests/bytecode.test.ts index dd3ccd6..bcb82eb 100644 --- a/tests/bytecode.test.ts +++ b/tests/bytecode.test.ts @@ -25,11 +25,12 @@ test("string compilation", () => { test("MAKE_FUNCTION - basic function", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION () #5 + MAKE_FUNCTION () .body PUSH 0 PUSH 0 CALL HALT + .body: PUSH 42 RETURN `) @@ -41,13 +42,14 @@ test("MAKE_FUNCTION - basic function", async () => { test("MAKE_FUNCTION - function with parameters", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (x y) #7 + MAKE_FUNCTION (x y) .add PUSH 10 PUSH 20 PUSH 2 PUSH 0 CALL HALT + .add: LOAD x LOAD y ADD @@ -61,12 +63,13 @@ test("MAKE_FUNCTION - function with parameters", async () => { test("MAKE_FUNCTION - function with default parameters", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (x y=100) #6 + MAKE_FUNCTION (x y=100) .add PUSH 10 PUSH 1 PUSH 0 CALL HALT + .add: LOAD x LOAD y ADD @@ -80,7 +83,7 @@ test("MAKE_FUNCTION - function with default parameters", async () => { test("MAKE_FUNCTION - tail recursive countdown", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (n) #8 + MAKE_FUNCTION (n) .countdown STORE countdown LOAD countdown PUSH 5 @@ -88,12 +91,14 @@ test("MAKE_FUNCTION - tail recursive countdown", async () => { PUSH 0 CALL HALT + .countdown: LOAD n PUSH 0 EQ - JUMP_IF_FALSE #2 + JUMP_IF_FALSE .recurse PUSH "done" RETURN + .recurse: LOAD countdown LOAD n PUSH 1 @@ -110,11 +115,12 @@ test("MAKE_FUNCTION - tail recursive countdown", async () => { test("MAKE_FUNCTION - multiple default values", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (a=1 b=2 c=3) #5 + MAKE_FUNCTION (a=1 b=2 c=3) .sum PUSH 0 PUSH 0 CALL HALT + .sum: LOAD a LOAD b LOAD c @@ -130,11 +136,12 @@ test("MAKE_FUNCTION - multiple default values", async () => { test("MAKE_FUNCTION - default with string", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (name="World") #5 + MAKE_FUNCTION (name="World") .greet PUSH 0 PUSH 0 CALL HALT + .greet: LOAD name RETURN `) @@ -167,18 +174,18 @@ test("semicolon comments are ignored", () => { test("semicolon comments work with functions", async () => { const bytecode = toBytecode(` - MAKE_FUNCTION (x y) #7 ; function with two params - PUSH 10 ; first arg - PUSH 20 ; second arg - PUSH 2 ; positional count - PUSH 0 ; named count - CALL ; call the function + MAKE_FUNCTION (x y) .add ; function with two params + PUSH 10 ; first arg + PUSH 20 ; second arg + PUSH 2 ; positional count + PUSH 0 ; named count + CALL ; call the function HALT - ; Function body starts here - LOAD x ; load first param - LOAD y ; load second param - ADD ; add them - RETURN ; return result + .add: ; Function body starts here + LOAD x ; load first param + LOAD y ; load second param + ADD ; add them + RETURN ; return result `) const vm = new VM(bytecode) @@ -186,3 +193,55 @@ test("semicolon comments work with functions", async () => { expect(result).toEqual({ type: 'number', value: 30 }) }) +test("labels - basic jump", async () => { + const bytecode = toBytecode(` + JUMP .end + PUSH 999 + .end: + PUSH 42 + `) + + const vm = new VM(bytecode) + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 42 }) +}) + +test("labels - conditional jump", async () => { + const bytecode = toBytecode(` + PUSH 1 + JUMP_IF_FALSE .else + PUSH 10 + JUMP .end + .else: + PUSH 20 + .end: + `) + + const vm = new VM(bytecode) + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 10 }) +}) + +test("labels - loop", async () => { + const bytecode = toBytecode(` + PUSH 0 + STORE i + .loop: + LOAD i + PUSH 3 + LT + JUMP_IF_FALSE .end + LOAD i + PUSH 1 + ADD + STORE i + JUMP .loop + .end: + LOAD i + `) + + const vm = new VM(bytecode) + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 3 }) +}) + diff --git a/tests/exceptions.test.ts b/tests/exceptions.test.ts index bedb92a..80c06c8 100644 --- a/tests/exceptions.test.ts +++ b/tests/exceptions.test.ts @@ -5,12 +5,13 @@ import { run } from "#index" test("PUSH_TRY and POP_TRY - no exception thrown", async () => { // Try block that completes successfully const str = ` - PUSH_TRY #5 + PUSH_TRY .catch PUSH 42 PUSH 10 ADD POP_TRY HALT + .catch: PUSH 999 HALT ` @@ -20,11 +21,12 @@ test("PUSH_TRY and POP_TRY - no exception thrown", async () => { test("THROW - catch exception with error value", async () => { // Try block that throws an exception const str = ` - PUSH_TRY #5 + PUSH_TRY .catch PUSH "error occurred" THROW PUSH 999 HALT + .catch: HALT ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error occurred' }) @@ -41,16 +43,18 @@ test("THROW - uncaught exception throws JS error", async () => { test("THROW - exception with nested try blocks", async () => { // Nested try blocks, inner one catches const str = ` - PUSH_TRY #10 - PUSH_TRY #6 + PUSH_TRY .outer_catch + PUSH_TRY .inner_catch PUSH "inner error" THROW PUSH 999 HALT + .inner_catch: STORE err POP_TRY LOAD err HALT + .outer_catch: PUSH "outer error" HALT ` @@ -60,13 +64,15 @@ test("THROW - exception with nested try blocks", async () => { test("THROW - exception skips outer handler", async () => { // Nested try blocks, inner doesn't catch, outer does const str = ` - PUSH_TRY #8 - PUSH_TRY #6 + PUSH_TRY .outer_catch + PUSH_TRY .inner_catch PUSH "error message" THROW HALT + .inner_catch: THROW HALT + .outer_catch: HALT ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error message' }) @@ -129,15 +135,16 @@ test("POP_TRY - error when no handler to pop", async () => { test("PUSH_FINALLY - finally executes after successful try", async () => { // Try block completes normally, compiler jumps to finally const str = ` - PUSH_TRY #8 - PUSH_FINALLY #9 + PUSH_TRY .catch + PUSH_FINALLY .finally PUSH 10 STORE x POP_TRY - JUMP #3 - HALT + JUMP .finally HALT + .catch: HALT + .finally: PUSH 100 LOAD x ADD @@ -149,13 +156,15 @@ test("PUSH_FINALLY - finally executes after successful try", async () => { test("PUSH_FINALLY - finally executes after exception", async () => { // Try block throws, THROW jumps to finally (skipping catch) const str = ` - PUSH_TRY #5 - PUSH_FINALLY #7 + PUSH_TRY .catch + PUSH_FINALLY .finally PUSH "error" THROW HALT + .catch: STORE err HALT + .finally: POP PUSH "finally ran" HALT @@ -166,13 +175,15 @@ test("PUSH_FINALLY - finally executes after exception", async () => { test("PUSH_FINALLY - finally without catch", async () => { // Try-finally without catch (compiler generates jump to finally) const str = ` - PUSH_TRY #7 - PUSH_FINALLY #7 + PUSH_TRY .catch + PUSH_FINALLY .finally PUSH 42 STORE x POP_TRY - JUMP #1 + JUMP .finally + .catch: HALT + .finally: LOAD x PUSH 10 ADD @@ -184,20 +195,22 @@ test("PUSH_FINALLY - finally without catch", async () => { test("PUSH_FINALLY - nested try-finally blocks", async () => { // Nested try-finally blocks with compiler-generated jumps const str = ` - PUSH_TRY #11 - PUSH_FINALLY #14 - PUSH_TRY #9 - PUSH_FINALLY #10 + PUSH_TRY .outer_catch + PUSH_FINALLY .outer_finally + PUSH_TRY .inner_catch + PUSH_FINALLY .inner_finally PUSH 1 POP_TRY - JUMP #3 - HALT - HALT + JUMP .inner_finally + .inner_catch: HALT + .inner_finally: PUSH 10 POP_TRY - JUMP #1 + JUMP .outer_finally + .outer_catch: HALT + .outer_finally: ADD HALT ` @@ -206,7 +219,8 @@ test("PUSH_FINALLY - nested try-finally blocks", async () => { test("PUSH_FINALLY - error when no handler", async () => { const str = ` - PUSH_FINALLY #5 + PUSH_FINALLY .finally + .finally: ` await expect(run(toBytecode(str))).rejects.toThrow('PUSH_FINALLY: no exception handler to modify') }) diff --git a/tests/tail-call.test.ts b/tests/tail-call.test.ts index 76bbfcf..0012987 100644 --- a/tests/tail-call.test.ts +++ b/tests/tail-call.test.ts @@ -9,7 +9,7 @@ test("TAIL_CALL - basic tail recursive countdown", async () => { // return countdown(n - 1) // tail call // } const bytecode = toBytecode(` - MAKE_FUNCTION (n) #8 + MAKE_FUNCTION (n) .countdown STORE countdown LOAD countdown PUSH 5 @@ -17,12 +17,14 @@ test("TAIL_CALL - basic tail recursive countdown", async () => { PUSH 0 CALL HALT + .countdown: LOAD n PUSH 0 EQ - JUMP_IF_FALSE #2 + JUMP_IF_FALSE .recurse PUSH "done" RETURN + .recurse: LOAD countdown LOAD n PUSH 1 @@ -42,7 +44,7 @@ test("TAIL_CALL - tail recursive sum with accumulator", async () => { // return sum(n - 1, acc + n) // tail call // } const bytecode = toBytecode(` - MAKE_FUNCTION (n acc) #9 + MAKE_FUNCTION (n acc) .sum STORE sum LOAD sum PUSH 10 @@ -51,12 +53,14 @@ test("TAIL_CALL - tail recursive sum with accumulator", async () => { PUSH 0 CALL HALT + .sum: LOAD n PUSH 0 EQ - JUMP_IF_FALSE #2 + JUMP_IF_FALSE .recurse LOAD acc RETURN + .recurse: LOAD sum LOAD n PUSH 1 @@ -77,7 +81,7 @@ test("TAIL_CALL - doesn't overflow stack with deep recursion", async () => { // This would overflow the stack with regular CALL // but should work fine with TAIL_CALL const bytecode = toBytecode(` - MAKE_FUNCTION (n) #8 + MAKE_FUNCTION (n) .deep STORE deep LOAD deep PUSH 10000 @@ -85,12 +89,14 @@ test("TAIL_CALL - doesn't overflow stack with deep recursion", async () => { PUSH 0 CALL HALT + .deep: LOAD n PUSH 0 LTE - JUMP_IF_FALSE #2 + JUMP_IF_FALSE .recurse PUSH "success" RETURN + .recurse: LOAD deep LOAD n PUSH 1 @@ -109,9 +115,9 @@ test("TAIL_CALL - tail call to different function", async () => { // function even(n) { return n === 0 ? true : odd(n - 1) } // function odd(n) { return n === 0 ? false : even(n - 1) } const bytecode = toBytecode(` - MAKE_FUNCTION (n) #10 + MAKE_FUNCTION (n) .even STORE even - MAKE_FUNCTION (n) #23 + MAKE_FUNCTION (n) .odd STORE odd LOAD even PUSH 7 @@ -119,12 +125,14 @@ test("TAIL_CALL - tail call to different function", async () => { PUSH 0 CALL HALT + .even: LOAD n PUSH 0 EQ - JUMP_IF_FALSE #2 + JUMP_IF_FALSE .even_recurse PUSH true RETURN + .even_recurse: LOAD odd LOAD n PUSH 1 @@ -132,12 +140,14 @@ test("TAIL_CALL - tail call to different function", async () => { PUSH 1 PUSH 0 TAIL_CALL + .odd: LOAD n PUSH 0 EQ - JUMP_IF_FALSE #2 + JUMP_IF_FALSE .odd_recurse PUSH false RETURN + .odd_recurse: LOAD even LOAD n PUSH 1