forked from defunkt/ReefVM
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
|
**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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user