test examples
This commit is contained in:
parent
405cc23b3d
commit
ec2b1a9b22
91
SPEC.md
91
SPEC.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,14 +174,14 @@ test("semicolon comments are ignored", () => {
|
|||
|
||||
test("semicolon comments work with functions", async () => {
|
||||
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 20 ; second arg
|
||||
PUSH 2 ; positional count
|
||||
PUSH 0 ; named count
|
||||
CALL ; call the function
|
||||
HALT
|
||||
; Function body starts here
|
||||
.add: ; Function body starts here
|
||||
LOAD x ; load first param
|
||||
LOAD y ; load second param
|
||||
ADD ; add them
|
||||
|
|
@ -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 })
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user