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
**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> | 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 <index>
MAKE_FUNCTION <params> .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 <factorial_def>
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

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<string, number>()
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))

View File

@ -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)

View File

@ -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 })
})

View File

@ -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')
})

View File

@ -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