Passing NULL to a function triggers its default value
This commit is contained in:
parent
d7a971db24
commit
3e2e68b31f
28
GUIDE.md
28
GUIDE.md
|
|
@ -179,6 +179,17 @@ PUSH 1 ; Named count
|
||||||
CALL
|
CALL
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Null triggers defaults**: Pass `null` to use default values:
|
||||||
|
```
|
||||||
|
; Function: greet(name='Guest', msg='Hello')
|
||||||
|
LOAD greet
|
||||||
|
PUSH null ; Use default for 'name'
|
||||||
|
PUSH "Hi" ; Provide 'msg'
|
||||||
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL ; → "Hi, Guest"
|
||||||
|
```
|
||||||
|
|
||||||
## Opcodes
|
## Opcodes
|
||||||
|
|
||||||
### Stack
|
### Stack
|
||||||
|
|
@ -748,11 +759,24 @@ Variable and function parameter names support Unicode and emoji:
|
||||||
|
|
||||||
### Parameter Binding Priority
|
### Parameter Binding Priority
|
||||||
For function calls, parameters bound in order:
|
For function calls, parameters bound in order:
|
||||||
1. Positional argument (if provided)
|
1. Named argument (if provided and matches param name)
|
||||||
2. Named argument (if provided and matches param name)
|
2. Positional argument (if provided)
|
||||||
3. Default value (if defined)
|
3. Default value (if defined)
|
||||||
4. Null
|
4. Null
|
||||||
|
|
||||||
|
**Null Triggering Defaults**: Passing `null` as an argument (positional or named) triggers the default value if one exists. This allows callers to explicitly "opt-in" to defaults:
|
||||||
|
```
|
||||||
|
# Function with defaults: greet(name='Guest', greeting='Hello')
|
||||||
|
LOAD greet
|
||||||
|
PUSH null # Triggers default: name='Guest'
|
||||||
|
PUSH 'Hi' # Provided: greeting='Hi'
|
||||||
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL # Returns "Hi, Guest"
|
||||||
|
```
|
||||||
|
|
||||||
|
This works for both ReefVM functions and native TypeScript functions. If no default exists, `null` is bound as-is.
|
||||||
|
|
||||||
### Exception Handlers
|
### Exception Handlers
|
||||||
- PUSH_TRY uses absolute addresses for catch blocks
|
- PUSH_TRY uses absolute addresses for catch blocks
|
||||||
- Nested try blocks form a stack
|
- Nested try blocks form a stack
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ Commands: `clear`, `reset`, `exit`.
|
||||||
- Variadic functions with positional rest parameters (`...rest`)
|
- Variadic functions with positional rest parameters (`...rest`)
|
||||||
- Named arguments (named) that collect unmatched named args into a dict (`@named`)
|
- Named arguments (named) that collect unmatched named args into a dict (`@named`)
|
||||||
- Mixed positional and named arguments with proper priority binding
|
- Mixed positional and named arguments with proper priority binding
|
||||||
|
- Default parameter values with null-triggering: passing `null` explicitly uses the default value
|
||||||
- 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 with auto-wrapping for native TypeScript types
|
- Native function interop with auto-wrapping for native TypeScript types
|
||||||
|
|
@ -60,3 +61,4 @@ Commands: `clear`, `reset`, `exit`.
|
||||||
- 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: Functions can collect unmatched named arguments into a dict using `@named` syntax
|
- Named parameters: Functions can collect unmatched named arguments into a dict using `@named` syntax
|
||||||
- Argument binding priority: Named args bind to regular params first, with unmatched ones going to `@named`
|
- Argument binding priority: Named args bind to regular params first, with unmatched ones going to `@named`
|
||||||
|
- Null triggers defaults: Passing `null` to a parameter with a default value explicitly uses that default (applies to both ReefVM and native functions)
|
||||||
31
SPEC.md
31
SPEC.md
|
|
@ -474,11 +474,19 @@ The created function captures `currentScope` as its `parentScope`.
|
||||||
3. Default value (if defined)
|
3. Default value (if defined)
|
||||||
4. Null
|
4. Null
|
||||||
|
|
||||||
|
**Null Value Semantics**:
|
||||||
|
- Passing `null` as an argument explicitly triggers the default value (if one exists)
|
||||||
|
- This allows callers to "opt-in" to defaults even when providing arguments positionally
|
||||||
|
- If no default exists, `null` is bound as-is
|
||||||
|
- This applies to both ReefVM functions and native TypeScript functions
|
||||||
|
- Example: `fn(null, 20)` where `fn(x=10, y)` binds `x=10` (default triggered), `y=20`
|
||||||
|
|
||||||
**Named Args Handling**:
|
**Named Args Handling**:
|
||||||
- Named args that match fixed parameter names are bound to those params
|
- Named args that match fixed parameter names are bound to those params
|
||||||
- If the function has `named: true`, remaining named args (that don't match any fixed param) are collected into the last parameter as a dict
|
- If the function has `named: true`, remaining named args (that don't match any fixed param) are collected into the last parameter as a dict
|
||||||
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict
|
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict
|
||||||
- **Native functions support named arguments** - parameter names are extracted from the function signature at call time
|
- **Native functions support named arguments** - parameter names are extracted from the function signature at call time
|
||||||
|
- Passing `null` via named args also triggers defaults: `fn(x=null)` triggers `x`'s default
|
||||||
|
|
||||||
**Errors**: Throws if top of stack is not a function (or native function)
|
**Errors**: Throws if top of stack is not a function (or native function)
|
||||||
|
|
||||||
|
|
@ -908,6 +916,29 @@ PUSH 1 # namedCount
|
||||||
CALL
|
CALL
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Null Triggering Default Values
|
||||||
|
```
|
||||||
|
# Function: greet(name='Guest', greeting='Hello')
|
||||||
|
MAKE_FUNCTION (name='Guest' greeting='Hello') .greet_body
|
||||||
|
STORE 'greet'
|
||||||
|
JUMP .main
|
||||||
|
.greet_body:
|
||||||
|
LOAD 'greeting'
|
||||||
|
PUSH ', '
|
||||||
|
ADD
|
||||||
|
LOAD 'name'
|
||||||
|
ADD
|
||||||
|
RETURN
|
||||||
|
.main:
|
||||||
|
# Call with null for first param - triggers default
|
||||||
|
LOAD 'greet'
|
||||||
|
PUSH null # name will use default 'Guest'
|
||||||
|
PUSH 'Hi' # greeting='Hi' (provided)
|
||||||
|
PUSH 2 # positionalCount
|
||||||
|
PUSH 0 # namedCount
|
||||||
|
CALL # Returns "Hi, Guest"
|
||||||
|
```
|
||||||
|
|
||||||
### Tail Recursive Function
|
### Tail Recursive Function
|
||||||
```
|
```
|
||||||
MAKE_FUNCTION (n acc) .factorial_body
|
MAKE_FUNCTION (n acc) .factorial_body
|
||||||
|
|
|
||||||
30
src/vm.ts
30
src/vm.ts
|
|
@ -602,16 +602,25 @@ export class VM {
|
||||||
let nativePositionalArgIndex = 0
|
let nativePositionalArgIndex = 0
|
||||||
|
|
||||||
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
||||||
|
// Note: null values trigger defaults (null acts as "use default")
|
||||||
for (let i = 0; i < nativeFixedParamCount; i++) {
|
for (let i = 0; i < nativeFixedParamCount; i++) {
|
||||||
const paramName = paramInfo.params[i]!
|
const paramName = paramInfo.params[i]!
|
||||||
|
let paramValue: Value | undefined
|
||||||
|
|
||||||
// Check if named argument was provided for this param
|
// Check if named argument was provided for this param
|
||||||
if (namedArgs.has(paramName)) {
|
if (namedArgs.has(paramName)) {
|
||||||
nativeArgs.push(namedArgs.get(paramName)!)
|
paramValue = namedArgs.get(paramName)!
|
||||||
namedArgs.delete(paramName) // Remove from named args so it won't go to @named
|
namedArgs.delete(paramName) // Remove from named args so it won't go to @named
|
||||||
} else if (nativePositionalArgIndex < positionalArgs.length) {
|
} else if (nativePositionalArgIndex < positionalArgs.length) {
|
||||||
nativeArgs.push(positionalArgs[nativePositionalArgIndex]!)
|
paramValue = positionalArgs[nativePositionalArgIndex]!
|
||||||
nativePositionalArgIndex++
|
nativePositionalArgIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the parameter value is null and a default exists, use the default
|
||||||
|
if (paramValue?.type === 'null' && paramInfo.defaults[paramName] !== undefined) {
|
||||||
|
nativeArgs.push(paramInfo.defaults[paramName]!)
|
||||||
|
} else if (paramValue) {
|
||||||
|
nativeArgs.push(paramValue)
|
||||||
} else if (paramInfo.defaults[paramName] !== undefined) {
|
} else if (paramInfo.defaults[paramName] !== undefined) {
|
||||||
nativeArgs.push(paramInfo.defaults[paramName]!)
|
nativeArgs.push(paramInfo.defaults[paramName]!)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -696,16 +705,29 @@ export class VM {
|
||||||
let positionalArgIndex = 0
|
let positionalArgIndex = 0
|
||||||
|
|
||||||
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
||||||
|
// Note: null values trigger defaults (null acts as "use default")
|
||||||
for (let i = 0; i < fixedParamCount; i++) {
|
for (let i = 0; i < fixedParamCount; i++) {
|
||||||
const paramName = fn.params[i]!
|
const paramName = fn.params[i]!
|
||||||
|
let paramValue: Value | undefined
|
||||||
|
|
||||||
// Check if named argument was provided for this param
|
// Check if named argument was provided for this param
|
||||||
if (namedArgs.has(paramName)) {
|
if (namedArgs.has(paramName)) {
|
||||||
this.scope.set(paramName, namedArgs.get(paramName)!)
|
paramValue = namedArgs.get(paramName)!
|
||||||
namedArgs.delete(paramName) // Remove from named args so it won't go to named
|
namedArgs.delete(paramName) // Remove from named args so it won't go to named
|
||||||
} else if (positionalArgIndex < positionalArgs.length) {
|
} else if (positionalArgIndex < positionalArgs.length) {
|
||||||
this.scope.set(paramName, positionalArgs[positionalArgIndex]!)
|
paramValue = positionalArgs[positionalArgIndex]!
|
||||||
positionalArgIndex++
|
positionalArgIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the parameter value is null and a default exists, use the default
|
||||||
|
if (paramValue && paramValue.type === 'null' && fn.defaults[paramName] !== undefined) {
|
||||||
|
const defaultIdx = fn.defaults[paramName]!
|
||||||
|
const defaultValue = this.constants[defaultIdx]!
|
||||||
|
if (defaultValue.type === 'function_def')
|
||||||
|
throw new Error('Default value cannot be a function definition')
|
||||||
|
this.scope.set(paramName, defaultValue)
|
||||||
|
} else if (paramValue) {
|
||||||
|
this.scope.set(paramName, paramValue)
|
||||||
} 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]!
|
||||||
|
|
|
||||||
|
|
@ -519,3 +519,206 @@ test("TRY_CALL - with string format", async () => {
|
||||||
const result = await run(bytecode)
|
const result = await run(bytecode)
|
||||||
expect(result).toEqual({ type: 'number', value: 100 })
|
expect(result).toEqual({ type: 'number', value: 100 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("CALL - passing null triggers default value for single parameter", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x=42"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// Passing null should trigger the default value of 42
|
||||||
|
expect(result).toEqual({ type: 'number', value: 42 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - passing null triggers default value for multiple parameters", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["a=10", "b=20", "c=30"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "a"],
|
||||||
|
["LOAD", "b"],
|
||||||
|
["ADD"],
|
||||||
|
["LOAD", "c"],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", 5],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// a=5 (provided), b=20 (null triggers default), c=30 (null triggers default)
|
||||||
|
// Result: 5 + 20 + 30 = 55
|
||||||
|
expect(result).toEqual({ type: 'number', value: 55 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null in middle parameter triggers default", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x=100", "y=200", "z=300"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["LOAD", "y"],
|
||||||
|
["ADD"],
|
||||||
|
["LOAD", "z"],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// x=1, y=200 (null triggers default), z=3
|
||||||
|
// Result: 1 + 200 + 3 = 204
|
||||||
|
expect(result).toEqual({ type: 'number', value: 204 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null with named arguments triggers default", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x=50", "y=75"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["LOAD", "y"],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", "x"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", "y"],
|
||||||
|
["PUSH", 25],
|
||||||
|
["PUSH", 0],
|
||||||
|
["PUSH", 2],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// x=50 (null triggers default), y=25 (provided via named arg)
|
||||||
|
// Result: 50 + 25 = 75
|
||||||
|
expect(result).toEqual({ type: 'number', value: 75 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null with string default value", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["name='Guest'"], ".body"],
|
||||||
|
["STORE", "greet"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["PUSH", "Hello, "],
|
||||||
|
["LOAD", "name"],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "greet"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// Passing null should trigger the default value 'Guest'
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'Hello, Guest' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null with no default still results in null", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// No default value, so null should be returned
|
||||||
|
expect(result).toEqual({ type: 'null', value: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null triggers default with variadic parameters", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x=99", "...rest"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 2],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 4],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// x should be 99 (null triggers default), rest gets [1, 2, 3]
|
||||||
|
expect(result).toEqual({ type: 'number', value: 99 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null triggers default with @named parameter", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x=777", "@named"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", "foo"],
|
||||||
|
["PUSH", "bar"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 1],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// x should be 777 (null triggers default)
|
||||||
|
expect(result).toEqual({ type: 'number', value: 777 })
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -2361,3 +2361,121 @@ test('uncaught native function error crashes VM', async () => {
|
||||||
|
|
||||||
await expect(vm.run()).rejects.toThrow('uncaught error')
|
await expect(vm.run()).rejects.toThrow('uncaught error')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('native function - null triggers default value for single parameter', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["LOAD", "greet"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.set('greet', (name = 'Guest') => `Hello, ${name}!`)
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
// Passing null should trigger the default value 'Guest'
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'Hello, Guest!' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function - null triggers default value for multiple parameters', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["LOAD", "add"],
|
||||||
|
["PUSH", 5],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.set('add', (a, b = 10, c = 20) => a + b + c)
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
// a=5 (provided), b=10 (null triggers default), c=20 (null triggers default)
|
||||||
|
// Result: 5 + 10 + 20 = 35
|
||||||
|
expect(result).toEqual({ type: 'number', value: 35 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function - null in middle parameter triggers default', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["LOAD", "calculate"],
|
||||||
|
["PUSH", 100],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 50],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.set('calculate', (x, y = 200, z) => x + y + z)
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
// x=100, y=200 (null triggers default), z=50
|
||||||
|
// Result: 100 + 200 + 50 = 350
|
||||||
|
expect(result).toEqual({ type: 'number', value: 350 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function - null with named arguments triggers default', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["LOAD", "format"],
|
||||||
|
["PUSH", "name"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", "age"],
|
||||||
|
["PUSH", 30],
|
||||||
|
["PUSH", 0],
|
||||||
|
["PUSH", 2],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.set('format', (name = 'Anonymous', age) => `${name} is ${age} years old`)
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
// name='Anonymous' (null triggers default), age=30 (provided via named arg)
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'Anonymous is 30 years old' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function - null with no default still passes null', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["LOAD", "checkNull"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.set('checkNull', (value) => value === null ? 'got null' : 'got value')
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
// No default value, so null should be passed through
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'got null' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('native function - mix of null and provided values with defaults', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["LOAD", "build"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", "Bob"],
|
||||||
|
["PUSH", 2],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.set('build', (prefix = 'Mr.', name, suffix = 'Jr.') => `${prefix} ${name} ${suffix}`)
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
// prefix='Mr.' (null triggers default), name='Bob', suffix='Jr.' (default, not provided)
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'Mr. Bob Jr.' })
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user