variadic and named args
This commit is contained in:
parent
e75d119ba8
commit
db4f332472
11
README.md
11
README.md
|
|
@ -77,7 +77,7 @@ It's where Shrimp live.
|
||||||
|
|
||||||
## Test Status
|
## Test Status
|
||||||
|
|
||||||
✅ **83 tests passing** covering:
|
✅ **91 tests passing** covering:
|
||||||
- All stack operations (PUSH, POP, DUP)
|
- All stack operations (PUSH, POP, DUP)
|
||||||
- All arithmetic operations (ADD, SUB, MUL, DIV, MOD)
|
- All arithmetic operations (ADD, SUB, MUL, DIV, MOD)
|
||||||
- All comparison operations (EQ, NEQ, LT, GT, LTE, GTE)
|
- All comparison operations (EQ, NEQ, LT, GT, LTE, GTE)
|
||||||
|
|
@ -87,7 +87,9 @@ It's where Shrimp live.
|
||||||
- All array operations (MAKE_ARRAY, ARRAY_GET, ARRAY_SET, ARRAY_PUSH, ARRAY_LEN)
|
- All array operations (MAKE_ARRAY, ARRAY_GET, ARRAY_SET, ARRAY_PUSH, ARRAY_LEN)
|
||||||
- All dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS)
|
- All dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS)
|
||||||
- Function operations (MAKE_FUNCTION, CALL, TAIL_CALL, RETURN) with parameter binding
|
- Function operations (MAKE_FUNCTION, CALL, TAIL_CALL, RETURN) with parameter binding
|
||||||
- **Variadic functions** with positional rest parameters
|
- **Variadic functions** with positional rest parameters (`...rest`)
|
||||||
|
- **Named arguments (kwargs)** that collect unmatched named args into a dict (`@kwargs`)
|
||||||
|
- **Mixed positional and named arguments** with proper priority binding
|
||||||
- **Tail call optimization** with unbounded recursion (10,000+ iterations without stack overflow)
|
- **Tail call optimization** with unbounded recursion (10,000+ iterations without stack overflow)
|
||||||
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
||||||
- Native function interop (CALL_NATIVE) with sync and async functions
|
- Native function interop (CALL_NATIVE) with sync and async functions
|
||||||
|
|
@ -99,6 +101,5 @@ It's where Shrimp live.
|
||||||
- **Simple truthiness**: Only `null` and `false` are falsy (unlike JavaScript where `0`, `""`, etc. are also falsy)
|
- **Simple truthiness**: Only `null` and `false` are falsy (unlike JavaScript where `0`, `""`, etc. are also falsy)
|
||||||
- **Short-circuiting via compiler**: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation
|
- **Short-circuiting via compiler**: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation
|
||||||
- **Variadic parameters**: Functions can collect remaining positional arguments into an array using `...rest` syntax
|
- **Variadic parameters**: Functions can collect remaining positional arguments into an array using `...rest` syntax
|
||||||
|
- **Named parameters (kwargs)**: Functions can collect unmatched named arguments into a dict using `@kwargs` syntax
|
||||||
🚧 **Still TODO**:
|
- **Argument binding priority**: Named args can bind to regular params first, with unmatched ones going to `@kwargs`
|
||||||
- Advanced function features (kwargs, named arguments in CALL)
|
|
||||||
41
SPEC.md
41
SPEC.md
|
|
@ -316,38 +316,43 @@ The constant must be a `function_def` with:
|
||||||
The created function captures `currentScope` as its `parentScope`.
|
The created function captures `currentScope` as its `parentScope`.
|
||||||
|
|
||||||
#### CALL
|
#### CALL
|
||||||
**Operand**: Either:
|
**Operand**: None
|
||||||
- Number: positional argument count
|
|
||||||
- Object: `{ positional: number, named: number }`
|
|
||||||
|
|
||||||
**Stack**: [fn, arg1, arg2, ..., name1, val1, name2, val2, ...] → [returnValue]
|
**Stack**: [fn, arg1, arg2, ..., name1, val1, name2, val2, ..., positionalCount, namedCount] → [returnValue]
|
||||||
|
|
||||||
**Behavior**:
|
**Behavior**:
|
||||||
1. Pop function from stack
|
1. Pop namedCount from stack (top of stack)
|
||||||
2. Pop named arguments (name/value pairs) according to operand
|
2. Pop positionalCount from stack
|
||||||
3. Pop positional arguments according to operand
|
3. Pop named arguments (name/value pairs) from stack
|
||||||
4. Mark current frame (if exists) as break target (`isBreakTarget = true`)
|
4. Pop positional arguments from stack
|
||||||
5. Push new call frame with current PC and scope
|
5. Pop function from stack
|
||||||
6. Create new scope with function's parentScope as parent
|
6. Mark current frame (if exists) as break target (`isBreakTarget = true`)
|
||||||
7. Bind parameters:
|
7. Push new call frame with current PC and scope
|
||||||
|
8. Create new scope with function's parentScope as parent
|
||||||
|
9. Bind parameters:
|
||||||
- For regular functions: bind params by position, then by name, then defaults, then null
|
- For regular functions: bind params by position, then by name, then defaults, then null
|
||||||
- For variadic functions: bind fixed params, collect rest into array
|
- For variadic functions: bind fixed params, collect rest into array
|
||||||
- For kwargs functions: bind fixed params, collect named args into dict
|
- For kwargs functions: bind fixed params by position/name, collect unmatched named args into dict
|
||||||
8. Set currentScope to new scope
|
10. Set currentScope to new scope
|
||||||
9. Jump to function body
|
11. Jump to function body
|
||||||
|
|
||||||
**Parameter Binding Priority**:
|
**Parameter Binding Priority** (for fixed params):
|
||||||
1. Named argument (if provided)
|
1. Named argument (if provided and matches param name)
|
||||||
2. Positional argument (if provided)
|
2. Positional argument (if provided)
|
||||||
3. Default value (if defined)
|
3. Default value (if defined)
|
||||||
4. Null
|
4. Null
|
||||||
|
|
||||||
|
**Named Args Handling**:
|
||||||
|
- Named args that match fixed parameter names are bound to those params
|
||||||
|
- Remaining named args (that don't match any fixed param) are collected into `@kwargs` dict
|
||||||
|
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to kwargs
|
||||||
|
|
||||||
**Errors**: Throws if top of stack is not a function
|
**Errors**: Throws if top of stack is not a function
|
||||||
|
|
||||||
#### TAIL_CALL
|
#### TAIL_CALL
|
||||||
**Operand**: Same as CALL
|
**Operand**: None
|
||||||
**Effect**: Same as CALL, but reuses current call frame
|
**Effect**: Same as CALL, but reuses current call frame
|
||||||
**Stack**: Same as CALL
|
**Stack**: [fn, arg1, arg2, ..., name1, val1, name2, val2, ..., positionalCount, namedCount] → [returnValue]
|
||||||
|
|
||||||
**Behavior**: Identical to CALL except:
|
**Behavior**: Identical to CALL except:
|
||||||
- Does NOT push a new call frame
|
- Does NOT push a new call frame
|
||||||
|
|
|
||||||
131
src/vm.ts
131
src/vm.ts
|
|
@ -333,11 +333,30 @@ export class VM {
|
||||||
break
|
break
|
||||||
|
|
||||||
case OpCode.CALL: {
|
case OpCode.CALL: {
|
||||||
const argCount = instruction.operand as number
|
// Pop named count from stack (top)
|
||||||
|
const namedCount = toNumber(this.stack.pop()!)
|
||||||
|
|
||||||
const args: Value[] = []
|
// Pop positional count from stack
|
||||||
for (let i = 0; i < argCount; i++)
|
const positionalCount = toNumber(this.stack.pop()!)
|
||||||
args.unshift(this.stack.pop()!)
|
|
||||||
|
// Pop named arguments (name-value pairs) from stack
|
||||||
|
// Stack has: ... key1 value1 key2 value2 (top)
|
||||||
|
// So we pop value2, key2, value1, key1
|
||||||
|
const namedArgs = new Map<string, Value>()
|
||||||
|
const namedPairs: Array<{ key: string; value: Value }> = []
|
||||||
|
for (let i = 0; i < namedCount; i++) {
|
||||||
|
const value = this.stack.pop()!
|
||||||
|
const key = this.stack.pop()!
|
||||||
|
namedPairs.unshift({ key: toString(key), value })
|
||||||
|
}
|
||||||
|
for (const pair of namedPairs) {
|
||||||
|
namedArgs.set(pair.key, pair.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop positional arguments from stack
|
||||||
|
const positionalArgs: Value[] = []
|
||||||
|
for (let i = 0; i < positionalCount; i++)
|
||||||
|
positionalArgs.unshift(this.stack.pop()!)
|
||||||
|
|
||||||
const fn = this.stack.pop()!
|
const fn = this.stack.pop()!
|
||||||
|
|
||||||
|
|
@ -355,13 +374,21 @@ export class VM {
|
||||||
|
|
||||||
this.scope = new Scope(fn.parentScope)
|
this.scope = new Scope(fn.parentScope)
|
||||||
|
|
||||||
const fixedParamCount = fn.variadic ? fn.params.length - 1 : fn.params.length
|
// Determine how many params are fixed (excluding variadic and named)
|
||||||
|
let fixedParamCount = fn.params.length
|
||||||
|
if (fn.variadic) fixedParamCount--
|
||||||
|
if (fn.named) fixedParamCount--
|
||||||
|
|
||||||
|
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
||||||
for (let i = 0; i < fixedParamCount; i++) {
|
for (let i = 0; i < fixedParamCount; i++) {
|
||||||
const paramName = fn.params[i]!
|
const paramName = fn.params[i]!
|
||||||
const argValue = args[i]
|
|
||||||
|
|
||||||
if (argValue !== undefined) {
|
// Check if named argument was provided for this param
|
||||||
this.scope.set(paramName, argValue)
|
if (namedArgs.has(paramName)) {
|
||||||
|
this.scope.set(paramName, namedArgs.get(paramName)!)
|
||||||
|
namedArgs.delete(paramName) // Remove from named args so it won't go to kwargs
|
||||||
|
} else if (positionalArgs[i] !== undefined) {
|
||||||
|
this.scope.set(paramName, positionalArgs[i]!)
|
||||||
} else if (fn.defaults[paramName] !== undefined) {
|
} else if (fn.defaults[paramName] !== undefined) {
|
||||||
const defaultIdx = fn.defaults[paramName]!
|
const defaultIdx = fn.defaults[paramName]!
|
||||||
const defaultValue = this.constants[defaultIdx]!
|
const defaultValue = this.constants[defaultIdx]!
|
||||||
|
|
@ -373,40 +400,75 @@ export class VM {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle variadic parameter (collect remaining positional args)
|
||||||
if (fn.variadic) {
|
if (fn.variadic) {
|
||||||
const variadicParamName = fn.params[fn.params.length - 1]!
|
const variadicParamName = fn.params[fn.params.length - (fn.named ? 2 : 1)]!
|
||||||
const remainingArgs = args.slice(fixedParamCount)
|
const remainingArgs = positionalArgs.slice(fixedParamCount)
|
||||||
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle named parameter (collect remaining named args that didn't match params)
|
||||||
|
if (fn.named) {
|
||||||
|
const namedParamName = fn.params[fn.params.length - 1]!
|
||||||
|
const kwargsDict = new Map<string, Value>()
|
||||||
|
for (const [key, value] of namedArgs) {
|
||||||
|
kwargsDict.set(key, value)
|
||||||
|
}
|
||||||
|
this.scope.set(namedParamName, { type: 'dict', value: kwargsDict })
|
||||||
|
}
|
||||||
|
|
||||||
// subtract 1 because pc was incremented
|
// subtract 1 because pc was incremented
|
||||||
this.pc = fn.body - 1
|
this.pc = fn.body - 1
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case OpCode.TAIL_CALL: {
|
case OpCode.TAIL_CALL: {
|
||||||
const tailArgCount = instruction.operand as number
|
// Pop named count from stack (top)
|
||||||
|
const tailNamedCount = toNumber(this.stack.pop()!)
|
||||||
|
|
||||||
const args: Value[] = []
|
// Pop positional count from stack
|
||||||
for (let i = 0; i < tailArgCount; i++)
|
const tailPositionalCount = toNumber(this.stack.pop()!)
|
||||||
args.unshift(this.stack.pop()!)
|
|
||||||
|
|
||||||
const fn = this.stack.pop()!
|
// Pop named arguments (name-value pairs) from stack
|
||||||
|
const tailNamedArgs = new Map<string, Value>()
|
||||||
|
const tailNamedPairs: Array<{ key: string; value: Value }> = []
|
||||||
|
for (let i = 0; i < tailNamedCount; i++) {
|
||||||
|
const value = this.stack.pop()!
|
||||||
|
const key = this.stack.pop()!
|
||||||
|
tailNamedPairs.unshift({ key: toString(key), value })
|
||||||
|
}
|
||||||
|
for (const pair of tailNamedPairs) {
|
||||||
|
tailNamedArgs.set(pair.key, pair.value)
|
||||||
|
}
|
||||||
|
|
||||||
if (fn.type !== 'function')
|
// Pop positional arguments from stack
|
||||||
|
const tailPositionalArgs: Value[] = []
|
||||||
|
for (let i = 0; i < tailPositionalCount; i++)
|
||||||
|
tailPositionalArgs.unshift(this.stack.pop()!)
|
||||||
|
|
||||||
|
const tailFn = this.stack.pop()!
|
||||||
|
|
||||||
|
if (tailFn.type !== 'function')
|
||||||
throw new Error('TAIL_CALL: not a function')
|
throw new Error('TAIL_CALL: not a function')
|
||||||
|
|
||||||
this.scope = new Scope(fn.parentScope)
|
this.scope = new Scope(tailFn.parentScope)
|
||||||
|
|
||||||
const fixedParamCount = fn.variadic ? fn.params.length - 1 : fn.params.length
|
// Determine how many params are fixed (excluding variadic and named)
|
||||||
for (let i = 0; i < fixedParamCount; i++) {
|
let tailFixedParamCount = tailFn.params.length
|
||||||
const paramName = fn.params[i]!
|
if (tailFn.variadic) tailFixedParamCount--
|
||||||
const argValue = args[i]
|
if (tailFn.named) tailFixedParamCount--
|
||||||
|
|
||||||
if (argValue !== undefined) {
|
// Bind fixed parameters
|
||||||
this.scope.set(paramName, argValue)
|
for (let i = 0; i < tailFixedParamCount; i++) {
|
||||||
} else if (fn.defaults[paramName] !== undefined) {
|
const paramName = tailFn.params[i]!
|
||||||
const defaultIdx = fn.defaults[paramName]!
|
|
||||||
|
if (tailNamedArgs.has(paramName)) {
|
||||||
|
this.scope.set(paramName, tailNamedArgs.get(paramName)!)
|
||||||
|
tailNamedArgs.delete(paramName)
|
||||||
|
} else if (tailPositionalArgs[i] !== undefined) {
|
||||||
|
this.scope.set(paramName, tailPositionalArgs[i]!)
|
||||||
|
} else if (tailFn.defaults[paramName] !== undefined) {
|
||||||
|
const defaultIdx = tailFn.defaults[paramName]!
|
||||||
const defaultValue = this.constants[defaultIdx]!
|
const defaultValue = this.constants[defaultIdx]!
|
||||||
if (defaultValue.type === 'function_def')
|
if (defaultValue.type === 'function_def')
|
||||||
throw new Error('Default value cannot be a function definition')
|
throw new Error('Default value cannot be a function definition')
|
||||||
|
|
@ -416,14 +478,25 @@ export class VM {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fn.variadic) {
|
// Handle variadic parameter
|
||||||
const variadicParamName = fn.params[fn.params.length - 1]!
|
if (tailFn.variadic) {
|
||||||
const remainingArgs = args.slice(fixedParamCount)
|
const variadicParamName = tailFn.params[tailFn.params.length - (tailFn.named ? 2 : 1)]!
|
||||||
|
const remainingArgs = tailPositionalArgs.slice(tailFixedParamCount)
|
||||||
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle named parameter
|
||||||
|
if (tailFn.named) {
|
||||||
|
const namedParamName = tailFn.params[tailFn.params.length - 1]!
|
||||||
|
const kwargsDict = new Map<string, Value>()
|
||||||
|
for (const [key, value] of tailNamedArgs) {
|
||||||
|
kwargsDict.set(key, value)
|
||||||
|
}
|
||||||
|
this.scope.set(namedParamName, { type: 'dict', value: kwargsDict })
|
||||||
|
}
|
||||||
|
|
||||||
// subtract 1 because PC was incremented
|
// subtract 1 because PC was incremented
|
||||||
this.pc = fn.body - 1
|
this.pc = tailFn.body - 1
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -521,8 +521,10 @@ 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 () #3
|
MAKE_FUNCTION () #5
|
||||||
CALL #0
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
BREAK
|
BREAK
|
||||||
`)
|
`)
|
||||||
|
|
@ -539,12 +541,16 @@ 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 () #4
|
MAKE_FUNCTION () #6
|
||||||
CALL #0
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
PUSH 42
|
PUSH 42
|
||||||
HALT
|
HALT
|
||||||
MAKE_FUNCTION () #7
|
MAKE_FUNCTION () #11
|
||||||
CALL #0
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
PUSH 99
|
PUSH 99
|
||||||
RETURN
|
RETURN
|
||||||
BREAK
|
BREAK
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,10 @@ test("string compilation", () => {
|
||||||
|
|
||||||
test("MAKE_FUNCTION - basic function", async () => {
|
test("MAKE_FUNCTION - basic function", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION () #3
|
MAKE_FUNCTION () #5
|
||||||
CALL #0
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
PUSH 42
|
PUSH 42
|
||||||
RETURN
|
RETURN
|
||||||
|
|
@ -39,10 +41,12 @@ 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) #5
|
MAKE_FUNCTION (x y) #7
|
||||||
PUSH 10
|
PUSH 10
|
||||||
PUSH 20
|
PUSH 20
|
||||||
CALL #2
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD x
|
LOAD x
|
||||||
LOAD y
|
LOAD y
|
||||||
|
|
@ -57,9 +61,11 @@ 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) #4
|
MAKE_FUNCTION (x y=100) #6
|
||||||
PUSH 10
|
PUSH 10
|
||||||
CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD x
|
LOAD x
|
||||||
LOAD y
|
LOAD y
|
||||||
|
|
@ -74,11 +80,13 @@ 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) #6
|
MAKE_FUNCTION (n) #8
|
||||||
STORE countdown
|
STORE countdown
|
||||||
LOAD countdown
|
LOAD countdown
|
||||||
PUSH 5
|
PUSH 5
|
||||||
CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD n
|
LOAD n
|
||||||
PUSH 0
|
PUSH 0
|
||||||
|
|
@ -90,7 +98,9 @@ test("MAKE_FUNCTION - tail recursive countdown", async () => {
|
||||||
LOAD n
|
LOAD n
|
||||||
PUSH 1
|
PUSH 1
|
||||||
SUB
|
SUB
|
||||||
TAIL_CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
TAIL_CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -100,8 +110,10 @@ 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) #3
|
MAKE_FUNCTION (a=1 b=2 c=3) #5
|
||||||
CALL #0
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD a
|
LOAD a
|
||||||
LOAD b
|
LOAD b
|
||||||
|
|
@ -118,8 +130,10 @@ 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") #3
|
MAKE_FUNCTION (name="World") #5
|
||||||
CALL #0
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD name
|
LOAD name
|
||||||
RETURN
|
RETURN
|
||||||
|
|
|
||||||
|
|
@ -82,19 +82,21 @@ test("THROW - exception unwinds call stack", async () => {
|
||||||
const vm = new VM({
|
const vm = new VM({
|
||||||
instructions: [
|
instructions: [
|
||||||
// 0: Main code with try block
|
// 0: Main code with try block
|
||||||
{ op: OpCode.PUSH_TRY, operand: 6 },
|
{ op: OpCode.PUSH_TRY, operand: 8 },
|
||||||
{ op: OpCode.MAKE_FUNCTION, operand: 0 },
|
{ op: OpCode.MAKE_FUNCTION, operand: 0 },
|
||||||
{ op: OpCode.CALL, operand: 0 },
|
{ op: OpCode.PUSH, operand: 3 }, // positionalCount = 0
|
||||||
|
{ op: OpCode.PUSH, operand: 3 }, // namedCount = 0
|
||||||
|
{ op: OpCode.CALL },
|
||||||
{ op: OpCode.POP_TRY },
|
{ op: OpCode.POP_TRY },
|
||||||
{ op: OpCode.HALT },
|
{ op: OpCode.HALT },
|
||||||
|
|
||||||
// 5: Not executed
|
// 7: Not executed
|
||||||
{ op: OpCode.PUSH, operand: 2 },
|
{ op: OpCode.PUSH, operand: 2 },
|
||||||
|
|
||||||
// 6: Catch block
|
// 8: Catch block
|
||||||
{ op: OpCode.HALT }, // error value on stack
|
{ op: OpCode.HALT }, // error value on stack
|
||||||
|
|
||||||
// 7: Function body (throws)
|
// 9: Function body (throws)
|
||||||
{ op: OpCode.PUSH, operand: 1 },
|
{ op: OpCode.PUSH, operand: 1 },
|
||||||
{ op: OpCode.THROW }
|
{ op: OpCode.THROW }
|
||||||
],
|
],
|
||||||
|
|
@ -103,12 +105,13 @@ test("THROW - exception unwinds call stack", async () => {
|
||||||
type: 'function_def',
|
type: 'function_def',
|
||||||
params: [],
|
params: [],
|
||||||
defaults: {},
|
defaults: {},
|
||||||
body: 7,
|
body: 9,
|
||||||
variadic: false,
|
variadic: false,
|
||||||
named: false
|
named: false
|
||||||
},
|
},
|
||||||
toValue('function error'),
|
toValue('function error'),
|
||||||
toValue(999)
|
toValue(999),
|
||||||
|
toValue(0) // constant for 0
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@ test("MAKE_FUNCTION - creates function with captured scope", async () => {
|
||||||
|
|
||||||
test("CALL and RETURN - basic function call", async () => {
|
test("CALL and RETURN - basic function call", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION () #3
|
MAKE_FUNCTION () #5
|
||||||
CALL #0
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
PUSH 42
|
PUSH 42
|
||||||
RETURN
|
RETURN
|
||||||
|
|
@ -30,9 +32,11 @@ test("CALL and RETURN - basic function call", async () => {
|
||||||
|
|
||||||
test("CALL and RETURN - function with one parameter", async () => {
|
test("CALL and RETURN - function with one parameter", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (x) #4
|
MAKE_FUNCTION (x) #6
|
||||||
PUSH 100
|
PUSH 100
|
||||||
CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD x
|
LOAD x
|
||||||
RETURN
|
RETURN
|
||||||
|
|
@ -44,10 +48,12 @@ test("CALL and RETURN - function with one parameter", async () => {
|
||||||
|
|
||||||
test("CALL and RETURN - function with two parameters", async () => {
|
test("CALL and RETURN - function with two parameters", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (a b) #5
|
MAKE_FUNCTION (a b) #7
|
||||||
PUSH 10
|
PUSH 10
|
||||||
PUSH 20
|
PUSH 20
|
||||||
CALL #2
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD a
|
LOAD a
|
||||||
LOAD b
|
LOAD b
|
||||||
|
|
@ -61,11 +67,13 @@ test("CALL and RETURN - function with two parameters", async () => {
|
||||||
|
|
||||||
test("CALL - variadic function with no fixed params", async () => {
|
test("CALL - variadic function with no fixed params", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (...args) #6
|
MAKE_FUNCTION (...args) #8
|
||||||
PUSH 1
|
PUSH 1
|
||||||
PUSH 2
|
PUSH 2
|
||||||
PUSH 3
|
PUSH 3
|
||||||
CALL #3
|
PUSH 3
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD args
|
LOAD args
|
||||||
RETURN
|
RETURN
|
||||||
|
|
@ -84,11 +92,13 @@ test("CALL - variadic function with no fixed params", async () => {
|
||||||
|
|
||||||
test("CALL - variadic function with one fixed param", async () => {
|
test("CALL - variadic function with one fixed param", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (x ...rest) #6
|
MAKE_FUNCTION (x ...rest) #8
|
||||||
PUSH 10
|
PUSH 10
|
||||||
PUSH 20
|
PUSH 20
|
||||||
PUSH 30
|
PUSH 30
|
||||||
CALL #3
|
PUSH 3
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD rest
|
LOAD rest
|
||||||
RETURN
|
RETURN
|
||||||
|
|
@ -107,12 +117,14 @@ test("CALL - variadic function with one fixed param", async () => {
|
||||||
|
|
||||||
test("CALL - variadic function with two fixed params", async () => {
|
test("CALL - variadic function with two fixed params", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (a b ...rest) #7
|
MAKE_FUNCTION (a b ...rest) #9
|
||||||
PUSH 1
|
PUSH 1
|
||||||
PUSH 2
|
PUSH 2
|
||||||
PUSH 3
|
PUSH 3
|
||||||
PUSH 4
|
PUSH 4
|
||||||
CALL #4
|
PUSH 4
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD rest
|
LOAD rest
|
||||||
RETURN
|
RETURN
|
||||||
|
|
@ -131,9 +143,11 @@ test("CALL - variadic function with two fixed params", async () => {
|
||||||
|
|
||||||
test("CALL - variadic function with no extra args", async () => {
|
test("CALL - variadic function with no extra args", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (x ...rest) #4
|
MAKE_FUNCTION (x ...rest) #6
|
||||||
PUSH 10
|
PUSH 10
|
||||||
CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD rest
|
LOAD rest
|
||||||
RETURN
|
RETURN
|
||||||
|
|
@ -146,8 +160,10 @@ test("CALL - variadic function with no extra args", async () => {
|
||||||
|
|
||||||
test("CALL - variadic function with defaults on fixed params", async () => {
|
test("CALL - variadic function with defaults on fixed params", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (x=5 ...rest) #3
|
MAKE_FUNCTION (x=5 ...rest) #5
|
||||||
CALL #0
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD x
|
LOAD x
|
||||||
RETURN
|
RETURN
|
||||||
|
|
@ -160,11 +176,13 @@ test("CALL - variadic function with defaults on fixed params", async () => {
|
||||||
|
|
||||||
test("TAIL_CALL - variadic function", async () => {
|
test("TAIL_CALL - variadic function", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION (x ...rest) #6
|
MAKE_FUNCTION (x ...rest) #8
|
||||||
PUSH 1
|
PUSH 1
|
||||||
PUSH 2
|
PUSH 2
|
||||||
PUSH 3
|
PUSH 3
|
||||||
CALL #3
|
PUSH 3
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD rest
|
LOAD rest
|
||||||
RETURN
|
RETURN
|
||||||
|
|
@ -180,3 +198,180 @@ test("TAIL_CALL - variadic function", async () => {
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("CALL - named args function with no fixed params", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
MAKE_FUNCTION (@kwargs) #9
|
||||||
|
PUSH "name"
|
||||||
|
PUSH "Bob"
|
||||||
|
PUSH "age"
|
||||||
|
PUSH 50
|
||||||
|
PUSH 0
|
||||||
|
PUSH 2
|
||||||
|
CALL
|
||||||
|
HALT
|
||||||
|
LOAD kwargs
|
||||||
|
RETURN
|
||||||
|
`)
|
||||||
|
|
||||||
|
const result = await new VM(bytecode).run()
|
||||||
|
expect(result.type).toBe('dict')
|
||||||
|
if (result.type === 'dict') {
|
||||||
|
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' })
|
||||||
|
expect(result.value.get('age')).toEqual({ type: 'number', value: 50 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - named args function with one fixed param", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
MAKE_FUNCTION (x @kwargs) #8
|
||||||
|
PUSH 10
|
||||||
|
PUSH "name"
|
||||||
|
PUSH "Alice"
|
||||||
|
PUSH 1
|
||||||
|
PUSH 1
|
||||||
|
CALL
|
||||||
|
HALT
|
||||||
|
LOAD kwargs
|
||||||
|
RETURN
|
||||||
|
`)
|
||||||
|
|
||||||
|
const result = await new VM(bytecode).run()
|
||||||
|
expect(result.type).toBe('dict')
|
||||||
|
if (result.type === 'dict') {
|
||||||
|
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' })
|
||||||
|
expect(result.value.size).toBe(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - named args with matching param name should bind to param not kwargs", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
MAKE_FUNCTION (name @kwargs) #8
|
||||||
|
PUSH "Bob"
|
||||||
|
PUSH "age"
|
||||||
|
PUSH 50
|
||||||
|
PUSH 1
|
||||||
|
PUSH 1
|
||||||
|
CALL
|
||||||
|
HALT
|
||||||
|
LOAD name
|
||||||
|
RETURN
|
||||||
|
`)
|
||||||
|
|
||||||
|
const result = await new VM(bytecode).run()
|
||||||
|
// name should be bound as regular param, not collected in kwargs
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'Bob' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - named args that match param names should not be in kwargs", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
MAKE_FUNCTION (name age @kwargs) #9
|
||||||
|
PUSH "name"
|
||||||
|
PUSH "Bob"
|
||||||
|
PUSH "city"
|
||||||
|
PUSH "NYC"
|
||||||
|
PUSH 0
|
||||||
|
PUSH 2
|
||||||
|
CALL
|
||||||
|
HALT
|
||||||
|
LOAD kwargs
|
||||||
|
RETURN
|
||||||
|
`)
|
||||||
|
|
||||||
|
const result = await new VM(bytecode).run()
|
||||||
|
expect(result.type).toBe('dict')
|
||||||
|
if (result.type === 'dict') {
|
||||||
|
// Only city should be in kwargs, name should be bound to param
|
||||||
|
expect(result.value.get('city')).toEqual({ type: 'string', value: 'NYC' })
|
||||||
|
expect(result.value.has('name')).toBe(false)
|
||||||
|
expect(result.value.size).toBe(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - mixed variadic and named args", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
MAKE_FUNCTION (x ...rest @kwargs) #10
|
||||||
|
PUSH 1
|
||||||
|
PUSH 2
|
||||||
|
PUSH 3
|
||||||
|
PUSH "name"
|
||||||
|
PUSH "Bob"
|
||||||
|
PUSH 3
|
||||||
|
PUSH 1
|
||||||
|
CALL
|
||||||
|
HALT
|
||||||
|
LOAD rest
|
||||||
|
RETURN
|
||||||
|
`)
|
||||||
|
|
||||||
|
const result = await new VM(bytecode).run()
|
||||||
|
// rest should have [2, 3]
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'array',
|
||||||
|
value: [
|
||||||
|
{ type: 'number', value: 2 },
|
||||||
|
{ type: 'number', value: 3 }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - mixed variadic and named args, check kwargs", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
MAKE_FUNCTION (x ...rest @kwargs) #10
|
||||||
|
PUSH 1
|
||||||
|
PUSH 2
|
||||||
|
PUSH 3
|
||||||
|
PUSH "name"
|
||||||
|
PUSH "Bob"
|
||||||
|
PUSH 3
|
||||||
|
PUSH 1
|
||||||
|
CALL
|
||||||
|
HALT
|
||||||
|
LOAD kwargs
|
||||||
|
RETURN
|
||||||
|
`)
|
||||||
|
|
||||||
|
const result = await new VM(bytecode).run()
|
||||||
|
expect(result.type).toBe('dict')
|
||||||
|
if (result.type === 'dict') {
|
||||||
|
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - named args with no extra named args", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
MAKE_FUNCTION (x @kwargs) #6
|
||||||
|
PUSH 10
|
||||||
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
|
HALT
|
||||||
|
LOAD kwargs
|
||||||
|
RETURN
|
||||||
|
`)
|
||||||
|
|
||||||
|
const result = await new VM(bytecode).run()
|
||||||
|
// kwargs should be empty dict
|
||||||
|
expect(result.type).toBe('dict')
|
||||||
|
if (result.type === 'dict') {
|
||||||
|
expect(result.value.size).toBe(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - named args with defaults on fixed params", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
MAKE_FUNCTION (x=5 @kwargs) #7
|
||||||
|
PUSH "name"
|
||||||
|
PUSH "Alice"
|
||||||
|
PUSH 0
|
||||||
|
PUSH 1
|
||||||
|
CALL
|
||||||
|
HALT
|
||||||
|
LOAD x
|
||||||
|
RETURN
|
||||||
|
`)
|
||||||
|
|
||||||
|
const result = await new VM(bytecode).run()
|
||||||
|
// x should use default value 5
|
||||||
|
expect(result).toEqual({ type: 'number', value: 5 })
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@ 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) #6
|
MAKE_FUNCTION (n) #8
|
||||||
STORE countdown
|
STORE countdown
|
||||||
LOAD countdown
|
LOAD countdown
|
||||||
PUSH 5
|
PUSH 5
|
||||||
CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD n
|
LOAD n
|
||||||
PUSH 0
|
PUSH 0
|
||||||
|
|
@ -25,7 +27,9 @@ test("TAIL_CALL - basic tail recursive countdown", async () => {
|
||||||
LOAD n
|
LOAD n
|
||||||
PUSH 1
|
PUSH 1
|
||||||
SUB
|
SUB
|
||||||
TAIL_CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
TAIL_CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await new VM(bytecode).run()
|
||||||
|
|
@ -38,12 +42,14 @@ 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) #7
|
MAKE_FUNCTION (n acc) #9
|
||||||
STORE sum
|
STORE sum
|
||||||
LOAD sum
|
LOAD sum
|
||||||
PUSH 10
|
PUSH 10
|
||||||
PUSH 0
|
PUSH 0
|
||||||
CALL #2
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD n
|
LOAD n
|
||||||
PUSH 0
|
PUSH 0
|
||||||
|
|
@ -58,7 +64,9 @@ test("TAIL_CALL - tail recursive sum with accumulator", async () => {
|
||||||
LOAD acc
|
LOAD acc
|
||||||
LOAD n
|
LOAD n
|
||||||
ADD
|
ADD
|
||||||
TAIL_CALL #2
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
TAIL_CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await new VM(bytecode).run()
|
||||||
|
|
@ -69,11 +77,13 @@ 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) #6
|
MAKE_FUNCTION (n) #8
|
||||||
STORE deep
|
STORE deep
|
||||||
LOAD deep
|
LOAD deep
|
||||||
PUSH 10000
|
PUSH 10000
|
||||||
CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD n
|
LOAD n
|
||||||
PUSH 0
|
PUSH 0
|
||||||
|
|
@ -85,7 +95,9 @@ test("TAIL_CALL - doesn't overflow stack with deep recursion", async () => {
|
||||||
LOAD n
|
LOAD n
|
||||||
PUSH 1
|
PUSH 1
|
||||||
SUB
|
SUB
|
||||||
TAIL_CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
TAIL_CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await new VM(bytecode).run()
|
||||||
|
|
@ -97,13 +109,15 @@ 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) #8
|
MAKE_FUNCTION (n) #10
|
||||||
STORE even
|
STORE even
|
||||||
MAKE_FUNCTION (n) #19
|
MAKE_FUNCTION (n) #23
|
||||||
STORE odd
|
STORE odd
|
||||||
LOAD even
|
LOAD even
|
||||||
PUSH 7
|
PUSH 7
|
||||||
CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
LOAD n
|
LOAD n
|
||||||
PUSH 0
|
PUSH 0
|
||||||
|
|
@ -115,7 +129,9 @@ test("TAIL_CALL - tail call to different function", async () => {
|
||||||
LOAD n
|
LOAD n
|
||||||
PUSH 1
|
PUSH 1
|
||||||
SUB
|
SUB
|
||||||
TAIL_CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
TAIL_CALL
|
||||||
LOAD n
|
LOAD n
|
||||||
PUSH 0
|
PUSH 0
|
||||||
EQ
|
EQ
|
||||||
|
|
@ -126,7 +142,9 @@ test("TAIL_CALL - tail call to different function", async () => {
|
||||||
LOAD n
|
LOAD n
|
||||||
PUSH 1
|
PUSH 1
|
||||||
SUB
|
SUB
|
||||||
TAIL_CALL #1
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
TAIL_CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await new VM(bytecode).run()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user