test examples

This commit is contained in:
Chris Wanstrath 2025-10-05 22:24:43 -07:00
parent 405cc23b3d
commit ec2b1a9b22
15 changed files with 426 additions and 212 deletions

91
SPEC.md
View File

@ -478,6 +478,35 @@ type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value;
**Effect**: Stop execution **Effect**: Stop execution
**Stack**: No change **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 ## Common Bytecode Patterns
### If-Else Statement ### If-Else Statement
@ -485,59 +514,61 @@ type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value;
LOAD 'x' LOAD 'x'
PUSH 5 PUSH 5
GT GT
JUMP_IF_FALSE 2 # skip then block, jump to else JUMP_IF_FALSE .else
# then block (N instructions) # then block
JUMP M # skip else block JUMP .end
.else:
# else block # else block
.end:
``` ```
### While Loop ### While Loop
``` ```
loop_start: .loop_start:
# condition # condition
JUMP_IF_FALSE N # jump past loop body JUMP_IF_FALSE .loop_end
# body (N-1 instructions) # body
JUMP -N # jump back to loop_start JUMP .loop_start
loop_end: .loop_end:
``` ```
### Function Definition ### Function Definition
``` ```
MAKE_FUNCTION <index> MAKE_FUNCTION <params> .function_body
STORE 'functionName' STORE 'functionName'
JUMP N # skip function body JUMP .skip_body
function_body: .function_body:
# function code (N instructions) # function code
RETURN RETURN
skip_body: .skip_body:
``` ```
### Try-Catch ### Try-Catch
``` ```
PUSH_TRY #3 ; Jump to catch block (3 instructions ahead) PUSH_TRY .catch
; try block ; try block
POP_TRY POP_TRY
JUMP #2 ; Jump past catch block JUMP .end
; catch: .catch:
STORE 'errorVar' ; Error is on stack STORE 'errorVar' ; Error is on stack
; catch block ; catch block
; end: .end:
``` ```
### Try-Catch-Finally ### Try-Catch-Finally
``` ```
PUSH_TRY #4 ; Jump to catch block (4 instructions ahead) PUSH_TRY .catch
PUSH_FINALLY #7 ; Jump to finally block (7 instructions ahead) PUSH_FINALLY .finally
; try block ; try block
POP_TRY POP_TRY
JUMP #5 ; Jump to finally JUMP .finally
; catch: .catch:
STORE 'errorVar' ; Error is on stack STORE 'errorVar' ; Error is on stack
; catch block ; catch block
JUMP #2 ; Jump to finally JUMP .finally
; finally: .finally:
; finally block (executes in both cases) ; finally block (executes in both cases)
; end: .end:
``` ```
### Named Function Call ### Named Function Call
@ -553,17 +584,17 @@ CALL
### Tail Recursive Function ### Tail Recursive Function
``` ```
MAKE_FUNCTION <factorial_def> MAKE_FUNCTION (n acc) .factorial_body
STORE 'factorial' STORE 'factorial'
JUMP 10 # skip to main JUMP .main
factorial_body: .factorial_body:
LOAD 'n' LOAD 'n'
PUSH 0 PUSH 0
EQ EQ
JUMP_IF_FALSE 2 # skip to recurse JUMP_IF_FALSE .recurse
LOAD 'acc' LOAD 'acc'
RETURN RETURN
recurse: .recurse:
LOAD 'factorial' LOAD 'factorial'
LOAD 'n' LOAD 'n'
PUSH 1 PUSH 1
@ -574,7 +605,7 @@ recurse:
PUSH 2 # positionalCount PUSH 2 # positionalCount
PUSH 0 # namedCount PUSH 0 # namedCount
TAIL_CALL # No stack growth! TAIL_CALL # No stack growth!
main: .main:
LOAD 'factorial' LOAD 'factorial'
PUSH 5 PUSH 5
PUSH 1 PUSH 1

View File

@ -23,8 +23,8 @@ You can run any example using the reef binary:
### Advanced Function Features ### Advanced Function Features
- **variadic.reef** - Variadic parameters that collect remaining positional arguments (`...rest`) - **variadic.reef** - Variadic parameters that collect remaining positional arguments (`...rest`)
- **kwargs.reef** - Named arguments that collect unmatched named args into a dict (`@kwargs`) - **named.reef** - Named arguments that collect unmatched named args into a dict (`@named`)
- **mixed-variadic-kwargs.reef** - Combining variadic positional and named arguments - **mixed-variadic-named.reef** - Combining variadic positional and named arguments
- **tail-recursion.reef** - Tail-recursive factorial using TAIL_CALL - **tail-recursion.reef** - Tail-recursive factorial using TAIL_CALL
- **closure.reef** - Closures that capture variables from outer scope - **closure.reef** - Closures that capture variables from outer scope
@ -38,12 +38,16 @@ The `.reef` files use the following syntax:
- Comments start with `;` - Comments start with `;`
- Instructions are written as `OPCODE operand` - Instructions are written as `OPCODE operand`
- Labels use `#N` for relative offsets - Labels:
- Function signatures: `MAKE_FUNCTION (params) #address` - 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)` - Fixed params: `(a b c)`
- Variadic: `(a ...rest)` - Variadic: `(a ...rest)`
- Named args: `(a @kwargs)` - Named args: `(a @named)`
- Mixed: `(a ...rest @kwargs)` - Mixed: `(a ...rest @named)`
## Function Calling Convention ## Function Calling Convention
@ -57,12 +61,19 @@ When calling functions, the stack must contain (bottom to top):
Example with positional arguments: Example with positional arguments:
``` ```
LOAD functionName ; push function onto stack MAKE_FUNCTION (a b) .add_body
PUSH arg1 ; first positional arg PUSH arg1 ; first positional arg
PUSH arg2 ; second positional arg PUSH arg2 ; second positional arg
PUSH 2 ; positional count PUSH 2 ; positional count
PUSH 0 ; named count PUSH 0 ; named count
CALL CALL
HALT
.add_body:
LOAD a
LOAD b
ADD
RETURN
``` ```
Example with named arguments: Example with named arguments:

View File

@ -1,6 +1,6 @@
; Closure example: counter function that captures state ; Closure example: counter function that captures state
; outer() returns inner() which increments and returns captured count ; outer() returns inner() which increments and returns captured count
MAKE_FUNCTION () #10 MAKE_FUNCTION () .outer_body
PUSH 0 PUSH 0
PUSH 0 PUSH 0
CALL CALL
@ -10,15 +10,17 @@ PUSH 0
PUSH 0 PUSH 0
CALL CALL
HALT HALT
; Outer function body
PUSH 0 .outer_body:
STORE count PUSH 0
MAKE_FUNCTION () #14 STORE count
RETURN MAKE_FUNCTION () .inner_body
; Inner function body (closure over count) RETURN
LOAD count
PUSH 1 .inner_body:
ADD LOAD count
STORE count PUSH 1
LOAD count ADD
RETURN STORE count
LOAD count
RETURN

View File

@ -1,19 +1,23 @@
; Try-catch-finally example ; Try-catch-finally example
PUSH_TRY #9 PUSH_TRY .catch
PUSH_FINALLY #16 PUSH_FINALLY .finally
PUSH 'Something went wrong!' PUSH 'Something went wrong!'
THROW THROW
PUSH 999 PUSH 999
POP_TRY POP_TRY
JUMP #4 JUMP .after_catch
HALT
; Catch block
STORE err
PUSH 'Caught: '
LOAD err
ADD
HALT
; Finally block
POP
PUSH 'Finally executed'
HALT HALT
.catch:
STORE err
PUSH 'Caught: '
LOAD err
ADD
HALT
.finally:
POP
PUSH 'Finally executed'
HALT
.after_catch:

View File

@ -1,17 +1,20 @@
; Loop example: count from 0 to 5 ; Loop example: count from 0 to 5
PUSH 0 PUSH 0
STORE i STORE i
; Loop condition
LOAD i .loop_start:
PUSH 5 LOAD i
LT PUSH 5
JUMP_IF_FALSE #6 LT
JUMP_IF_FALSE .loop_end
; Loop body ; Loop body
LOAD i LOAD i
PUSH 1 PUSH 1
ADD ADD
STORE i STORE i
JUMP #-9 JUMP .loop_start
; After loop
LOAD i .loop_end:
HALT LOAD i
HALT

View File

@ -1,6 +1,6 @@
; Mixed variadic and named arguments ; Mixed variadic and named arguments
; Function takes one fixed param, variadic positional, and kwargs ; Function takes one fixed param, variadic positional, and named
MAKE_FUNCTION (x ...rest @kwargs) #10 MAKE_FUNCTION (x ...rest @named) .fn_body
PUSH 1 PUSH 1
PUSH 2 PUSH 2
PUSH 3 PUSH 3
@ -10,9 +10,10 @@ PUSH 3
PUSH 1 PUSH 1
CALL CALL
HALT HALT
; Return array with all three parts
LOAD x .fn_body:
LOAD rest LOAD x
LOAD kwargs LOAD rest
MAKE_ARRAY #3 LOAD named
RETURN MAKE_ARRAY #3
RETURN

View File

@ -1,5 +1,6 @@
; Named arguments (kwargs) example ; Named arguments (named) example
MAKE_FUNCTION (x @kwargs) #9 MAKE_FUNCTION (x @named) .fn_body
PUSH 100
PUSH 'name' PUSH 'name'
PUSH 'Alice' PUSH 'Alice'
PUSH 'age' PUSH 'age'
@ -8,6 +9,7 @@ PUSH 1
PUSH 2 PUSH 2
CALL CALL
HALT HALT
; Return the kwargs dict
LOAD kwargs .fn_body:
RETURN LOAD named
RETURN

View File

@ -1,12 +1,14 @@
; Simple function that adds two numbers ; Simple function that adds two numbers
MAKE_FUNCTION (a b) #7 MAKE_FUNCTION (a b) .add_body
PUSH 10 PUSH 10
PUSH 20 PUSH 20
PUSH 2 PUSH 2
PUSH 0 PUSH 0
CALL CALL
HALT HALT
LOAD a
LOAD b .add_body:
ADD LOAD a
RETURN LOAD b
ADD
RETURN

View File

@ -1,6 +1,6 @@
; Tail-recursive factorial function ; Tail-recursive factorial function
; factorial(n, acc) = if n <= 1 then acc else factorial(n-1, n*acc) ; 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 DUP
PUSH 5 PUSH 5
PUSH 1 PUSH 1
@ -8,21 +8,23 @@ PUSH 2
PUSH 0 PUSH 0
CALL CALL
HALT HALT
; Function body
LOAD n .factorial_body:
PUSH 1 LOAD n
LTE PUSH 1
JUMP_IF_FALSE #2 LTE
LOAD acc JUMP_IF_FALSE .recurse
RETURN LOAD acc
; Tail recursive call RETURN
DUP
LOAD n .recurse:
PUSH 1 DUP
SUB LOAD n
LOAD n PUSH 1
LOAD acc SUB
MUL LOAD n
PUSH 2 LOAD acc
PUSH 0 MUL
TAIL_CALL PUSH 2
PUSH 0
TAIL_CALL

View File

@ -1,5 +1,5 @@
; Variadic function that sums all arguments ; Variadic function that sums all arguments
MAKE_FUNCTION (x ...rest) #19 MAKE_FUNCTION (x ...rest) .sum_body
PUSH 5 PUSH 5
PUSH 10 PUSH 10
PUSH 15 PUSH 15
@ -8,26 +8,32 @@ PUSH 4
PUSH 0 PUSH 0
CALL CALL
HALT HALT
; Function body: sum x and all rest elements
.sum_body:
LOAD x LOAD x
STORE sum STORE sum
PUSH 0 PUSH 0
STORE i STORE i
LOAD i
LOAD rest .loop_start:
ARRAY_LEN LOAD i
LT LOAD rest
JUMP_IF_FALSE #8 ARRAY_LEN
LOAD sum LT
LOAD rest JUMP_IF_FALSE .loop_end
LOAD i
ARRAY_GET LOAD sum
ADD LOAD rest
STORE sum LOAD i
LOAD i ARRAY_GET
PUSH 1 ADD
ADD STORE sum
STORE i LOAD i
JUMP #-18 PUSH 1
LOAD sum ADD
RETURN STORE i
JUMP .loop_start
.loop_end:
LOAD sum
RETURN

View File

@ -19,6 +19,7 @@ export type Constant =
// Parse bytecode from human-readable string format. // Parse bytecode from human-readable string format.
// Operand types are determined by prefix/literal: // Operand types are determined by prefix/literal:
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3) // #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) // name -> variable/function name (e.g., LOAD x, CALL_NATIVE add)
// 42 -> number constant (e.g., PUSH 42) // 42 -> number constant (e.g., PUSH 42)
// "str" -> string constant (e.g., PUSH "hello") // "str" -> string constant (e.g., PUSH "hello")
@ -27,8 +28,12 @@ export type Constant =
// false -> boolean constant (e.g., PUSH false) // false -> boolean constant (e.g., PUSH false)
// null -> null constant (e.g., PUSH null) // null -> null constant (e.g., PUSH null)
// //
// Labels:
// .label_name: -> label definition (marks current instruction position)
//
// Function definitions: // 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 y=42) #7 -> with defaults
// MAKE_FUNCTION (x ...rest) #7 -> variadic // MAKE_FUNCTION (x ...rest) #7 -> variadic
// MAKE_FUNCTION (x @named) #7 -> named // MAKE_FUNCTION (x @named) #7 -> named
@ -97,10 +102,9 @@ function parseFunctionParams(paramStr: string, constants: Constant[]): {
export function toBytecode(str: string): Bytecode /* throws */ { export function toBytecode(str: string): Bytecode /* throws */ {
const lines = str.trim().split("\n") const lines = str.trim().split("\n")
const bytecode: Bytecode = { // First pass: collect labels and their positions
instructions: [], const labels = new Map<string, number>()
constants: [] const cleanLines: string[] = []
}
for (let line of lines) { for (let line of lines) {
// Strip semicolon comments // Strip semicolon comments
@ -112,6 +116,24 @@ export function toBytecode(str: string): Bytecode /* throws */ {
const trimmed = line.trim() const trimmed = line.trim()
if (!trimmed) continue 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 [op, ...rest] = trimmed.split(/\s+/)
const opCode = OpCode[op as keyof typeof OpCode] 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 // Special handling for MAKE_FUNCTION with paren syntax
if (opCode === OpCode.MAKE_FUNCTION && operand.startsWith('(')) { if (opCode === OpCode.MAKE_FUNCTION && operand.startsWith('(')) {
// Parse: MAKE_FUNCTION (params) #body // Parse: MAKE_FUNCTION (params) #body or MAKE_FUNCTION (params) .label
const match = operand.match(/^(\(.*?\))\s+(#-?\d+)$/) const match = operand.match(/^(\(.*?\))\s+(#-?\d+|\.[a-zA-Z_][a-zA-Z0-9_]*)$/)
if (!match) { if (!match) {
throw new Error(`Invalid MAKE_FUNCTION syntax: ${operand}`) throw new Error(`Invalid MAKE_FUNCTION syntax: ${operand}`)
} }
const paramStr = match[1]! const paramStr = match[1]!
const bodyStr = match[2]! 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) 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 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 // immediate number
operandValue = parseInt(operand.slice(1)) operandValue = parseInt(operand.slice(1))

View File

@ -188,9 +188,10 @@ test("AND pattern - short circuits when false", async () => {
PUSH 0 PUSH 0
EQ EQ
DUP DUP
JUMP_IF_FALSE #2 JUMP_IF_FALSE .end
POP POP
PUSH 999 PUSH 999
.end:
` `
const result = await run(toBytecode(str)) const result = await run(toBytecode(str))
expect(result.type).toBe('boolean') expect(result.type).toBe('boolean')
@ -203,9 +204,10 @@ test("AND pattern - evaluates both when true", async () => {
const str = ` const str = `
PUSH 1 PUSH 1
DUP DUP
JUMP_IF_FALSE #2 JUMP_IF_FALSE .end
POP POP
PUSH 2 PUSH 2
.end:
` `
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
}) })
@ -214,9 +216,10 @@ test("OR pattern - short circuits when true", async () => {
const str = ` const str = `
PUSH 1 PUSH 1
DUP DUP
JUMP_IF_TRUE #2 JUMP_IF_TRUE .end
POP POP
PUSH 2 PUSH 2
.end:
` `
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 1 }) expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 1 })
}) })
@ -227,9 +230,10 @@ test("OR pattern - evaluates second when false", async () => {
PUSH 0 PUSH 0
EQ EQ
DUP DUP
JUMP_IF_TRUE #2 JUMP_IF_TRUE .end
POP POP
PUSH 2 PUSH 2
.end:
` `
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) 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) // 0 is truthy (unlike JS)
const str1 = ` const str1 = `
PUSH 0 PUSH 0
JUMP_IF_FALSE #1 JUMP_IF_FALSE .end
PUSH 1 PUSH 1
.end:
` `
expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 }) expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 })
// empty string is truthy (unlike JS) // empty string is truthy (unlike JS)
const str2 = ` const str2 = `
PUSH '' PUSH ''
JUMP_IF_FALSE #1 JUMP_IF_FALSE .end
PUSH 1 PUSH 1
.end:
` `
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 1 }) 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
PUSH 0 PUSH 0
EQ EQ
JUMP_IF_FALSE #1 JUMP_IF_FALSE .end
PUSH 999 PUSH 999
.end:
` `
expect(await run(toBytecode(str3))).toEqual({ type: 'number', value: 999 }) 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 () => { test("JUMP - relative jump forward", async () => {
const str = ` const str = `
PUSH 1 PUSH 1
JUMP #1 JUMP .skip
PUSH 100 PUSH 100
.skip:
PUSH 2 PUSH 2
` `
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 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 // Use forward jump to skip, demonstrating relative addressing
const str = ` const str = `
PUSH 100 PUSH 100
JUMP #2 JUMP .end
PUSH 200 PUSH 200
PUSH 300 PUSH 300
.end:
PUSH 400 PUSH 400
` `
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 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 1
PUSH 0 PUSH 0
EQ EQ
JUMP_IF_FALSE #1 JUMP_IF_FALSE .skip
PUSH 100 PUSH 100
.skip:
PUSH 42 PUSH 42
` `
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 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 () => { test("JUMP_IF_FALSE - no jump when true", async () => {
const str = ` const str = `
PUSH 1 PUSH 1
JUMP_IF_FALSE #1 JUMP_IF_FALSE .skip
PUSH 100 PUSH 100
.skip:
` `
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 }) 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 () => { test("JUMP_IF_TRUE - conditional jump when true", async () => {
const str = ` const str = `
PUSH 1 PUSH 1
JUMP_IF_TRUE #1 JUMP_IF_TRUE .skip
PUSH 100 PUSH 100
.skip:
PUSH 42 PUSH 42
` `
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 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 // BREAK requires a break target frame on the call stack
// A single function call has no previous frame to mark as break target // A single function call has no previous frame to mark as break target
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION () #5 MAKE_FUNCTION () .fn
PUSH 0 PUSH 0
PUSH 0 PUSH 0
CALL CALL
HALT HALT
.fn:
BREAK BREAK
`) `)
@ -541,18 +554,20 @@ test("BREAK - exits from nested function call", async () => {
// BREAK unwinds to the break target (the outer function's frame) // 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) // Main calls outer, outer calls inner, inner BREAKs back to outer's caller (main)
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION () #6 MAKE_FUNCTION () .outer
PUSH 0 PUSH 0
PUSH 0 PUSH 0
CALL CALL
PUSH 42 PUSH 42
HALT HALT
MAKE_FUNCTION () #11 .outer:
MAKE_FUNCTION () .inner
PUSH 0 PUSH 0
PUSH 0 PUSH 0
CALL CALL
PUSH 99 PUSH 99
RETURN RETURN
.inner:
BREAK BREAK
`) `)
@ -566,17 +581,19 @@ test("JUMP backward - simple loop", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
PUSH 0 PUSH 0
STORE counter STORE counter
.loop:
LOAD counter LOAD counter
PUSH 3 PUSH 3
EQ EQ
JUMP_IF_FALSE #2 JUMP_IF_FALSE .body
LOAD counter LOAD counter
HALT HALT
.body:
LOAD counter LOAD counter
PUSH 1 PUSH 1
ADD ADD
STORE counter STORE counter
JUMP #-11 JUMP .loop
`) `)
const result = await run(bytecode) const result = await run(bytecode)

View File

@ -25,11 +25,12 @@ test("string compilation", () => {
test("MAKE_FUNCTION - basic function", async () => { test("MAKE_FUNCTION - basic function", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION () #5 MAKE_FUNCTION () .body
PUSH 0 PUSH 0
PUSH 0 PUSH 0
CALL CALL
HALT HALT
.body:
PUSH 42 PUSH 42
RETURN RETURN
`) `)
@ -41,13 +42,14 @@ test("MAKE_FUNCTION - basic function", async () => {
test("MAKE_FUNCTION - function with parameters", async () => { test("MAKE_FUNCTION - function with parameters", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (x y) #7 MAKE_FUNCTION (x y) .add
PUSH 10 PUSH 10
PUSH 20 PUSH 20
PUSH 2 PUSH 2
PUSH 0 PUSH 0
CALL CALL
HALT HALT
.add:
LOAD x LOAD x
LOAD y LOAD y
ADD ADD
@ -61,12 +63,13 @@ test("MAKE_FUNCTION - function with parameters", async () => {
test("MAKE_FUNCTION - function with default parameters", async () => { test("MAKE_FUNCTION - function with default parameters", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (x y=100) #6 MAKE_FUNCTION (x y=100) .add
PUSH 10 PUSH 10
PUSH 1 PUSH 1
PUSH 0 PUSH 0
CALL CALL
HALT HALT
.add:
LOAD x LOAD x
LOAD y LOAD y
ADD ADD
@ -80,7 +83,7 @@ test("MAKE_FUNCTION - function with default parameters", async () => {
test("MAKE_FUNCTION - tail recursive countdown", async () => { test("MAKE_FUNCTION - tail recursive countdown", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (n) #8 MAKE_FUNCTION (n) .countdown
STORE countdown STORE countdown
LOAD countdown LOAD countdown
PUSH 5 PUSH 5
@ -88,12 +91,14 @@ test("MAKE_FUNCTION - tail recursive countdown", async () => {
PUSH 0 PUSH 0
CALL CALL
HALT HALT
.countdown:
LOAD n LOAD n
PUSH 0 PUSH 0
EQ EQ
JUMP_IF_FALSE #2 JUMP_IF_FALSE .recurse
PUSH "done" PUSH "done"
RETURN RETURN
.recurse:
LOAD countdown LOAD countdown
LOAD n LOAD n
PUSH 1 PUSH 1
@ -110,11 +115,12 @@ test("MAKE_FUNCTION - tail recursive countdown", async () => {
test("MAKE_FUNCTION - multiple default values", async () => { test("MAKE_FUNCTION - multiple default values", async () => {
const bytecode = toBytecode(` 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
PUSH 0 PUSH 0
CALL CALL
HALT HALT
.sum:
LOAD a LOAD a
LOAD b LOAD b
LOAD c LOAD c
@ -130,11 +136,12 @@ test("MAKE_FUNCTION - multiple default values", async () => {
test("MAKE_FUNCTION - default with string", async () => { test("MAKE_FUNCTION - default with string", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (name="World") #5 MAKE_FUNCTION (name="World") .greet
PUSH 0 PUSH 0
PUSH 0 PUSH 0
CALL CALL
HALT HALT
.greet:
LOAD name LOAD name
RETURN RETURN
`) `)
@ -167,14 +174,14 @@ test("semicolon comments are ignored", () => {
test("semicolon comments work with functions", async () => { test("semicolon comments work with functions", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (x y) #7 ; function with two params MAKE_FUNCTION (x y) .add ; function with two params
PUSH 10 ; first arg PUSH 10 ; first arg
PUSH 20 ; second arg PUSH 20 ; second arg
PUSH 2 ; positional count PUSH 2 ; positional count
PUSH 0 ; named count PUSH 0 ; named count
CALL ; call the function CALL ; call the function
HALT HALT
; Function body starts here .add: ; Function body starts here
LOAD x ; load first param LOAD x ; load first param
LOAD y ; load second param LOAD y ; load second param
ADD ; add them ADD ; add them
@ -186,3 +193,55 @@ test("semicolon comments work with functions", async () => {
expect(result).toEqual({ type: 'number', value: 30 }) 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 })
})

View File

@ -5,12 +5,13 @@ import { run } from "#index"
test("PUSH_TRY and POP_TRY - no exception thrown", async () => { test("PUSH_TRY and POP_TRY - no exception thrown", async () => {
// Try block that completes successfully // Try block that completes successfully
const str = ` const str = `
PUSH_TRY #5 PUSH_TRY .catch
PUSH 42 PUSH 42
PUSH 10 PUSH 10
ADD ADD
POP_TRY POP_TRY
HALT HALT
.catch:
PUSH 999 PUSH 999
HALT HALT
` `
@ -20,11 +21,12 @@ test("PUSH_TRY and POP_TRY - no exception thrown", async () => {
test("THROW - catch exception with error value", async () => { test("THROW - catch exception with error value", async () => {
// Try block that throws an exception // Try block that throws an exception
const str = ` const str = `
PUSH_TRY #5 PUSH_TRY .catch
PUSH "error occurred" PUSH "error occurred"
THROW THROW
PUSH 999 PUSH 999
HALT HALT
.catch:
HALT HALT
` `
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error occurred' }) 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 () => { test("THROW - exception with nested try blocks", async () => {
// Nested try blocks, inner one catches // Nested try blocks, inner one catches
const str = ` const str = `
PUSH_TRY #10 PUSH_TRY .outer_catch
PUSH_TRY #6 PUSH_TRY .inner_catch
PUSH "inner error" PUSH "inner error"
THROW THROW
PUSH 999 PUSH 999
HALT HALT
.inner_catch:
STORE err STORE err
POP_TRY POP_TRY
LOAD err LOAD err
HALT HALT
.outer_catch:
PUSH "outer error" PUSH "outer error"
HALT HALT
` `
@ -60,13 +64,15 @@ test("THROW - exception with nested try blocks", async () => {
test("THROW - exception skips outer handler", async () => { test("THROW - exception skips outer handler", async () => {
// Nested try blocks, inner doesn't catch, outer does // Nested try blocks, inner doesn't catch, outer does
const str = ` const str = `
PUSH_TRY #8 PUSH_TRY .outer_catch
PUSH_TRY #6 PUSH_TRY .inner_catch
PUSH "error message" PUSH "error message"
THROW THROW
HALT HALT
.inner_catch:
THROW THROW
HALT HALT
.outer_catch:
HALT HALT
` `
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error message' }) 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 () => { test("PUSH_FINALLY - finally executes after successful try", async () => {
// Try block completes normally, compiler jumps to finally // Try block completes normally, compiler jumps to finally
const str = ` const str = `
PUSH_TRY #8 PUSH_TRY .catch
PUSH_FINALLY #9 PUSH_FINALLY .finally
PUSH 10 PUSH 10
STORE x STORE x
POP_TRY POP_TRY
JUMP #3 JUMP .finally
HALT
HALT HALT
.catch:
HALT HALT
.finally:
PUSH 100 PUSH 100
LOAD x LOAD x
ADD ADD
@ -149,13 +156,15 @@ test("PUSH_FINALLY - finally executes after successful try", async () => {
test("PUSH_FINALLY - finally executes after exception", async () => { test("PUSH_FINALLY - finally executes after exception", async () => {
// Try block throws, THROW jumps to finally (skipping catch) // Try block throws, THROW jumps to finally (skipping catch)
const str = ` const str = `
PUSH_TRY #5 PUSH_TRY .catch
PUSH_FINALLY #7 PUSH_FINALLY .finally
PUSH "error" PUSH "error"
THROW THROW
HALT HALT
.catch:
STORE err STORE err
HALT HALT
.finally:
POP POP
PUSH "finally ran" PUSH "finally ran"
HALT HALT
@ -166,13 +175,15 @@ test("PUSH_FINALLY - finally executes after exception", async () => {
test("PUSH_FINALLY - finally without catch", async () => { test("PUSH_FINALLY - finally without catch", async () => {
// Try-finally without catch (compiler generates jump to finally) // Try-finally without catch (compiler generates jump to finally)
const str = ` const str = `
PUSH_TRY #7 PUSH_TRY .catch
PUSH_FINALLY #7 PUSH_FINALLY .finally
PUSH 42 PUSH 42
STORE x STORE x
POP_TRY POP_TRY
JUMP #1 JUMP .finally
.catch:
HALT HALT
.finally:
LOAD x LOAD x
PUSH 10 PUSH 10
ADD ADD
@ -184,20 +195,22 @@ test("PUSH_FINALLY - finally without catch", async () => {
test("PUSH_FINALLY - nested try-finally blocks", async () => { test("PUSH_FINALLY - nested try-finally blocks", async () => {
// Nested try-finally blocks with compiler-generated jumps // Nested try-finally blocks with compiler-generated jumps
const str = ` const str = `
PUSH_TRY #11 PUSH_TRY .outer_catch
PUSH_FINALLY #14 PUSH_FINALLY .outer_finally
PUSH_TRY #9 PUSH_TRY .inner_catch
PUSH_FINALLY #10 PUSH_FINALLY .inner_finally
PUSH 1 PUSH 1
POP_TRY POP_TRY
JUMP #3 JUMP .inner_finally
HALT .inner_catch:
HALT
HALT HALT
.inner_finally:
PUSH 10 PUSH 10
POP_TRY POP_TRY
JUMP #1 JUMP .outer_finally
.outer_catch:
HALT HALT
.outer_finally:
ADD ADD
HALT HALT
` `
@ -206,7 +219,8 @@ test("PUSH_FINALLY - nested try-finally blocks", async () => {
test("PUSH_FINALLY - error when no handler", async () => { test("PUSH_FINALLY - error when no handler", async () => {
const str = ` const str = `
PUSH_FINALLY #5 PUSH_FINALLY .finally
.finally:
` `
await expect(run(toBytecode(str))).rejects.toThrow('PUSH_FINALLY: no exception handler to modify') await expect(run(toBytecode(str))).rejects.toThrow('PUSH_FINALLY: no exception handler to modify')
}) })

View File

@ -9,7 +9,7 @@ test("TAIL_CALL - basic tail recursive countdown", async () => {
// return countdown(n - 1) // tail call // return countdown(n - 1) // tail call
// } // }
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (n) #8 MAKE_FUNCTION (n) .countdown
STORE countdown STORE countdown
LOAD countdown LOAD countdown
PUSH 5 PUSH 5
@ -17,12 +17,14 @@ test("TAIL_CALL - basic tail recursive countdown", async () => {
PUSH 0 PUSH 0
CALL CALL
HALT HALT
.countdown:
LOAD n LOAD n
PUSH 0 PUSH 0
EQ EQ
JUMP_IF_FALSE #2 JUMP_IF_FALSE .recurse
PUSH "done" PUSH "done"
RETURN RETURN
.recurse:
LOAD countdown LOAD countdown
LOAD n LOAD n
PUSH 1 PUSH 1
@ -42,7 +44,7 @@ test("TAIL_CALL - tail recursive sum with accumulator", async () => {
// return sum(n - 1, acc + n) // tail call // return sum(n - 1, acc + n) // tail call
// } // }
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (n acc) #9 MAKE_FUNCTION (n acc) .sum
STORE sum STORE sum
LOAD sum LOAD sum
PUSH 10 PUSH 10
@ -51,12 +53,14 @@ test("TAIL_CALL - tail recursive sum with accumulator", async () => {
PUSH 0 PUSH 0
CALL CALL
HALT HALT
.sum:
LOAD n LOAD n
PUSH 0 PUSH 0
EQ EQ
JUMP_IF_FALSE #2 JUMP_IF_FALSE .recurse
LOAD acc LOAD acc
RETURN RETURN
.recurse:
LOAD sum LOAD sum
LOAD n LOAD n
PUSH 1 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 // This would overflow the stack with regular CALL
// but should work fine with TAIL_CALL // but should work fine with TAIL_CALL
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (n) #8 MAKE_FUNCTION (n) .deep
STORE deep STORE deep
LOAD deep LOAD deep
PUSH 10000 PUSH 10000
@ -85,12 +89,14 @@ test("TAIL_CALL - doesn't overflow stack with deep recursion", async () => {
PUSH 0 PUSH 0
CALL CALL
HALT HALT
.deep:
LOAD n LOAD n
PUSH 0 PUSH 0
LTE LTE
JUMP_IF_FALSE #2 JUMP_IF_FALSE .recurse
PUSH "success" PUSH "success"
RETURN RETURN
.recurse:
LOAD deep LOAD deep
LOAD n LOAD n
PUSH 1 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 even(n) { return n === 0 ? true : odd(n - 1) }
// function odd(n) { return n === 0 ? false : even(n - 1) } // function odd(n) { return n === 0 ? false : even(n - 1) }
const bytecode = toBytecode(` const bytecode = toBytecode(`
MAKE_FUNCTION (n) #10 MAKE_FUNCTION (n) .even
STORE even STORE even
MAKE_FUNCTION (n) #23 MAKE_FUNCTION (n) .odd
STORE odd STORE odd
LOAD even LOAD even
PUSH 7 PUSH 7
@ -119,12 +125,14 @@ test("TAIL_CALL - tail call to different function", async () => {
PUSH 0 PUSH 0
CALL CALL
HALT HALT
.even:
LOAD n LOAD n
PUSH 0 PUSH 0
EQ EQ
JUMP_IF_FALSE #2 JUMP_IF_FALSE .even_recurse
PUSH true PUSH true
RETURN RETURN
.even_recurse:
LOAD odd LOAD odd
LOAD n LOAD n
PUSH 1 PUSH 1
@ -132,12 +140,14 @@ test("TAIL_CALL - tail call to different function", async () => {
PUSH 1 PUSH 1
PUSH 0 PUSH 0
TAIL_CALL TAIL_CALL
.odd:
LOAD n LOAD n
PUSH 0 PUSH 0
EQ EQ
JUMP_IF_FALSE #2 JUMP_IF_FALSE .odd_recurse
PUSH false PUSH false
RETURN RETURN
.odd_recurse:
LOAD even LOAD even
LOAD n LOAD n
PUSH 1 PUSH 1