TRY_CALL
This commit is contained in:
parent
8975bb91bd
commit
43842adc87
27
GUIDE.md
27
GUIDE.md
|
|
@ -219,6 +219,7 @@ CALL
|
|||
- `CALL` - Call function (see calling convention above)
|
||||
- `TAIL_CALL` - Tail-recursive call (no stack growth)
|
||||
- `RETURN` - Return from function (pops return value)
|
||||
- `TRY_CALL <name>` - Call function (if found), push value (if exists), or push name as string (if not found)
|
||||
- `BREAK` - Exit iterator/loop (unwinds to break target)
|
||||
|
||||
### Arrays
|
||||
|
|
@ -404,6 +405,32 @@ STORE factorial
|
|||
TAIL_CALL ; Reuses stack frame
|
||||
```
|
||||
|
||||
### Optional Function Calls (TRY_CALL)
|
||||
Call function if defined, otherwise use value or name as string:
|
||||
```
|
||||
; Define optional hook
|
||||
MAKE_FUNCTION () .onInit
|
||||
STORE onInit
|
||||
|
||||
; Later: call if defined, skip if not
|
||||
TRY_CALL onInit ; Calls onInit() if it's a function
|
||||
; Pushes value if it exists but isn't a function
|
||||
; Pushes "onInit" as string if undefined
|
||||
|
||||
; Use with values
|
||||
PUSH 42
|
||||
STORE answer
|
||||
TRY_CALL answer ; Pushes 42 (not a function)
|
||||
|
||||
; Use with undefined
|
||||
TRY_CALL unknown ; Pushes "unknown" as string
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Optional hooks/callbacks in DSLs
|
||||
- Shell-like languages where unknown identifiers become strings
|
||||
- Templating systems with optional transformers
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Truthiness
|
||||
|
|
|
|||
127
SPEC.md
127
SPEC.md
|
|
@ -46,7 +46,7 @@ type Value =
|
|||
|
||||
**toNumber**: number → identity, string → parseFloat (or 0), boolean → 1/0, others → 0
|
||||
|
||||
**toString**: string → identity, number → string, boolean → string, null → "null",
|
||||
**toString**: string → identity, number → string, boolean → string, null → "null",
|
||||
function → "<function>", array → "[item, item]", dict → "{key: value, ...}"
|
||||
|
||||
**isTrue**: Only `null` and `false` are falsy. Everything else (including `0`, `""`, empty arrays, empty dicts) is truthy.
|
||||
|
|
@ -124,26 +124,26 @@ type ExceptionHandler = {
|
|||
### Stack Operations
|
||||
|
||||
#### PUSH
|
||||
**Operand**: Index into constants pool (number)
|
||||
**Effect**: Push constant onto stack
|
||||
**Operand**: Index into constants pool (number)
|
||||
**Effect**: Push constant onto stack
|
||||
**Stack**: [] → [value]
|
||||
|
||||
#### POP
|
||||
**Operand**: None
|
||||
**Effect**: Discard top of stack
|
||||
**Operand**: None
|
||||
**Effect**: Discard top of stack
|
||||
**Stack**: [value] → []
|
||||
|
||||
#### DUP
|
||||
**Operand**: None
|
||||
**Effect**: Duplicate top of stack
|
||||
**Operand**: None
|
||||
**Effect**: Duplicate top of stack
|
||||
**Stack**: [value] → [value, value]
|
||||
|
||||
### Variable Operations
|
||||
|
||||
#### LOAD
|
||||
**Operand**: Variable name (string)
|
||||
**Effect**: Push variable value onto stack
|
||||
**Stack**: [] → [value]
|
||||
**Operand**: Variable name (string)
|
||||
**Effect**: Push variable value onto stack
|
||||
**Stack**: [] → [value]
|
||||
**Errors**: Throws if variable not found in scope chain
|
||||
|
||||
#### STORE
|
||||
|
|
@ -178,7 +178,7 @@ TRY_LOAD y ; Pushes "y" (variable doesn't exist)
|
|||
All arithmetic operations pop two values, perform operation, push result as number.
|
||||
|
||||
#### ADD
|
||||
**Stack**: [a, b] → [a + b]
|
||||
**Stack**: [a, b] → [a + b]
|
||||
**Note**: Only for numbers (use separate string concat if needed)
|
||||
|
||||
#### SUB
|
||||
|
|
@ -265,9 +265,9 @@ end:
|
|||
**Stack**: [condition] → []
|
||||
|
||||
#### BREAK
|
||||
**Operand**: None
|
||||
**Effect**: Unwind call stack until frame with `isBreakTarget = true`, resume there
|
||||
**Stack**: No change
|
||||
**Operand**: None
|
||||
**Effect**: Unwind call stack until frame with `isBreakTarget = true`, resume there
|
||||
**Stack**: No change
|
||||
**Errors**: Throws if no break target found
|
||||
|
||||
**Behavior**:
|
||||
|
|
@ -281,16 +281,16 @@ end:
|
|||
### Exception Handling
|
||||
|
||||
#### PUSH_TRY
|
||||
**Operand**: Catch block offset (number)
|
||||
**Effect**: Push exception handler
|
||||
**Operand**: Catch block offset (number)
|
||||
**Effect**: Push exception handler
|
||||
**Stack**: No change
|
||||
|
||||
Registers a try block. If THROW occurs before POP_TRY, execution jumps to catch address.
|
||||
|
||||
#### PUSH_FINALLY
|
||||
**Operand**: Finally block offset (number)
|
||||
**Effect**: Add finally address to most recent exception handler
|
||||
**Stack**: No change
|
||||
**Operand**: Finally block offset (number)
|
||||
**Effect**: Add finally address to most recent exception handler
|
||||
**Stack**: No change
|
||||
**Errors**: Throws if no exception handler to modify
|
||||
|
||||
Adds a finally block to the current try/catch. The finally block will execute whether an exception is thrown or not.
|
||||
|
|
@ -312,8 +312,8 @@ Adds a finally block to the current try/catch. The finally block will execute wh
|
|||
- Finally blocks should end with normal control flow (no special terminator needed)
|
||||
|
||||
#### THROW
|
||||
**Operand**: None
|
||||
**Effect**: Throw exception with error value from stack
|
||||
**Operand**: None
|
||||
**Effect**: Throw exception with error value from stack
|
||||
**Stack**: [errorValue] → (unwound)
|
||||
|
||||
**Behavior**:
|
||||
|
|
@ -333,8 +333,8 @@ Adds a finally block to the current try/catch. The finally block will execute wh
|
|||
### Function Operations
|
||||
|
||||
#### MAKE_FUNCTION
|
||||
**Operand**: Index into constants pool (number)
|
||||
**Effect**: Create function value, capturing current scope
|
||||
**Operand**: Index into constants pool (number)
|
||||
**Effect**: Create function value, capturing current scope
|
||||
**Stack**: [] → [function]
|
||||
|
||||
The constant must be a `function_def` with:
|
||||
|
|
@ -391,8 +391,8 @@ The created function captures `currentScope` as its `parentScope`.
|
|||
- Enables unbounded tail recursion without stack overflow
|
||||
|
||||
#### RETURN
|
||||
**Operand**: None
|
||||
**Effect**: Return from function
|
||||
**Operand**: None
|
||||
**Effect**: Return from function
|
||||
**Stack**: [returnValue] → (restored stack with returnValue on top)
|
||||
|
||||
**Behavior**:
|
||||
|
|
@ -404,19 +404,56 @@ The created function captures `currentScope` as its `parentScope`.
|
|||
|
||||
**Errors**: Throws if no call frame to return from
|
||||
|
||||
#### TRY_CALL
|
||||
**Operand**: Variable name (string)
|
||||
**Effect**: Conditionally call function or push value/string onto stack
|
||||
**Stack**: [] → [returnValue | value | name]
|
||||
**Errors**: Never throws (unlike CALL)
|
||||
|
||||
**Behavior**:
|
||||
1. Look up variable by name in scope chain
|
||||
2. **If variable is a function**: Call it with 0 arguments (no positional, no named) and push the returned value onto the stack.
|
||||
3. **If variable exists but is not a function**: Push the variable's value onto stack
|
||||
4. **If variable doesn't exist**: Push the variable name as a string onto stack
|
||||
|
||||
**Use Cases**:
|
||||
- DSL/templating languages with "call if callable, otherwise use as literal" semantics
|
||||
- Shell-like behavior where unknown identifiers become strings
|
||||
- Optional function hooks (call if defined, silently skip if not)
|
||||
|
||||
**Implementation Note**:
|
||||
- Uses intentional fall-through in VM switch statement from TRY_CALL to CALL case
|
||||
- When function is found, stacks are set up to match CALL's expectations exactly
|
||||
- No break target marking or frame pushing occurs when non-function value is found
|
||||
|
||||
**Example**:
|
||||
```
|
||||
MAKE_FUNCTION () .body
|
||||
STORE greet
|
||||
PUSH 42
|
||||
STORE answer
|
||||
TRY_CALL greet ; Calls function greet(), returns its value
|
||||
TRY_CALL answer ; Pushes 42 (number value)
|
||||
TRY_CALL unknown ; Pushes "unknown" (string)
|
||||
|
||||
.body:
|
||||
PUSH "Hello!"
|
||||
RETURN
|
||||
```
|
||||
|
||||
### Array Operations
|
||||
|
||||
#### MAKE_ARRAY
|
||||
**Operand**: Number of items (number)
|
||||
**Effect**: Create array from N stack items
|
||||
**Operand**: Number of items (number)
|
||||
**Effect**: Create array from N stack items
|
||||
**Stack**: [item1, item2, ..., itemN] → [array]
|
||||
|
||||
Items are popped in reverse order (item1 is array[0]).
|
||||
|
||||
#### ARRAY_GET
|
||||
**Operand**: None
|
||||
**Effect**: Get array element at index
|
||||
**Stack**: [array, index] → [value]
|
||||
**Operand**: None
|
||||
**Effect**: Get array element at index
|
||||
**Stack**: [array, index] → [value]
|
||||
**Errors**: Throws if not array or index out of bounds
|
||||
|
||||
Index is coerced to number and floored.
|
||||
|
|
@ -442,41 +479,41 @@ Index is coerced to number and floored.
|
|||
### Dictionary Operations
|
||||
|
||||
#### MAKE_DICT
|
||||
**Operand**: Number of key-value pairs (number)
|
||||
**Effect**: Create dict from N key-value pairs
|
||||
**Operand**: Number of key-value pairs (number)
|
||||
**Effect**: Create dict from N key-value pairs
|
||||
**Stack**: [key1, val1, key2, val2, ...] → [dict]
|
||||
|
||||
Keys are coerced to strings.
|
||||
|
||||
#### DICT_GET
|
||||
**Operand**: None
|
||||
**Effect**: Get dict value for key
|
||||
**Operand**: None
|
||||
**Effect**: Get dict value for key
|
||||
**Stack**: [dict, key] → [value]
|
||||
|
||||
Returns null if key not found. Key is coerced to string.
|
||||
Returns null if key not found. Key is coerced to string.
|
||||
**Errors**: Throws if not dict
|
||||
|
||||
#### DICT_SET
|
||||
**Operand**: None
|
||||
**Effect**: Set dict value for key (mutates dict)
|
||||
**Operand**: None
|
||||
**Effect**: Set dict value for key (mutates dict)
|
||||
**Stack**: [dict, key, value] → []
|
||||
|
||||
Key is coerced to string.
|
||||
Key is coerced to string.
|
||||
**Errors**: Throws if not dict
|
||||
|
||||
#### DICT_HAS
|
||||
**Operand**: None
|
||||
**Effect**: Check if key exists in dict
|
||||
**Operand**: None
|
||||
**Effect**: Check if key exists in dict
|
||||
**Stack**: [dict, key] → [boolean]
|
||||
|
||||
Key is coerced to string.
|
||||
Key is coerced to string.
|
||||
**Errors**: Throws if not dict
|
||||
|
||||
### TypeScript Interop
|
||||
|
||||
#### CALL_NATIVE
|
||||
**Operand**: Function name (string)
|
||||
**Effect**: Call registered TypeScript function
|
||||
**Operand**: Function name (string)
|
||||
**Effect**: Call registered TypeScript function
|
||||
**Stack**: [...args] → [returnValue]
|
||||
|
||||
**Behavior**:
|
||||
|
|
@ -501,8 +538,8 @@ type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value;
|
|||
### Special
|
||||
|
||||
#### HALT
|
||||
**Operand**: None
|
||||
**Effect**: Stop execution
|
||||
**Operand**: None
|
||||
**Effect**: Stop execution
|
||||
**Stack**: No change
|
||||
|
||||
## Label Syntax
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ type InstructionTuple =
|
|||
// Variables
|
||||
| ["LOAD", string]
|
||||
| ["STORE", string]
|
||||
| ["TRY_LOAD", string]
|
||||
|
||||
// Arithmetic
|
||||
| ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"]
|
||||
|
|
@ -54,6 +55,7 @@ type InstructionTuple =
|
|||
| ["CALL"]
|
||||
| ["TAIL_CALL"]
|
||||
| ["RETURN"]
|
||||
| ["TRY_CALL", string]
|
||||
|
||||
// Arrays
|
||||
| ["MAKE_ARRAY", number]
|
||||
|
|
@ -329,6 +331,8 @@ function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
|
|||
|
||||
case "LOAD":
|
||||
case "STORE":
|
||||
case "TRY_LOAD":
|
||||
case "TRY_CALL":
|
||||
case "CALL_NATIVE":
|
||||
operandValue = operand as string
|
||||
break
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export enum OpCode {
|
|||
CALL, // operand: none | stack: [fn, ...args, posCount, namedCount] → [result] | marks break target
|
||||
TAIL_CALL, // operand: none | stack: [fn, ...args, posCount, namedCount] → [result] | reuses frame
|
||||
RETURN, // operand: none | stack: [value] → (restored with value) | return from function
|
||||
TRY_CALL, /// operand: variable name (identifier) | stack: [] → [value] | call a function, load a variable, or load a string
|
||||
|
||||
// arrays
|
||||
MAKE_ARRAY, // operand: item count (number) | stack: [item1, ..., itemN] → [array]
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ const OPCODES_WITH_OPERANDS = new Set([
|
|||
OpCode.PUSH,
|
||||
OpCode.LOAD,
|
||||
OpCode.STORE,
|
||||
OpCode.TRY_LOAD,
|
||||
OpCode.TRY_CALL,
|
||||
OpCode.JUMP,
|
||||
OpCode.JUMP_IF_FALSE,
|
||||
OpCode.JUMP_IF_TRUE,
|
||||
|
|
@ -327,8 +329,9 @@ export function validateBytecode(source: string): ValidationResult {
|
|||
}
|
||||
}
|
||||
|
||||
// Validate variable names for LOAD/STORE
|
||||
if ((opCode === OpCode.LOAD || opCode === OpCode.STORE) &&
|
||||
// Validate variable names for LOAD/STORE/TRY_LOAD/TRY_CALL
|
||||
if ((opCode === OpCode.LOAD || opCode === OpCode.STORE ||
|
||||
opCode === OpCode.TRY_LOAD || opCode === OpCode.TRY_CALL) &&
|
||||
!isValidIdentifier(operand)) {
|
||||
errors.push({
|
||||
line: lineNum,
|
||||
|
|
|
|||
21
src/vm.ts
21
src/vm.ts
|
|
@ -353,6 +353,27 @@ export class VM {
|
|||
})
|
||||
break
|
||||
|
||||
// @ts-ignore
|
||||
case OpCode.TRY_CALL: {
|
||||
const varName = instruction.operand as string
|
||||
const value = this.scope.get(varName)
|
||||
|
||||
if (value?.type === 'function') {
|
||||
this.stack.push(value)
|
||||
this.stack.push(toValue(0))
|
||||
this.stack.push(toValue(0))
|
||||
// No `break` here -- we want to fall through to OpCode.CALL!
|
||||
} else if (value) {
|
||||
this.stack.push(value)
|
||||
break
|
||||
} else {
|
||||
this.stack.push(toValue(varName))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// don't put any `case` statement here - `TRY_CALL` MUST go before `CALL!`
|
||||
|
||||
case OpCode.CALL: {
|
||||
// Pop named count from stack (top)
|
||||
const namedCount = toNumber(this.stack.pop()!)
|
||||
|
|
|
|||
|
|
@ -375,3 +375,124 @@ test("CALL - named args with defaults on fixed params", async () => {
|
|||
// x should use default value 5
|
||||
expect(result).toEqual({ type: 'number', value: 5 })
|
||||
})
|
||||
|
||||
test("TRY_CALL - calls function if found", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["MAKE_FUNCTION", [], ".body"],
|
||||
["STORE", "myFunc"],
|
||||
["TRY_CALL", "myFunc"],
|
||||
["HALT"],
|
||||
[".body:"],
|
||||
["PUSH", 42],
|
||||
["RETURN"]
|
||||
])
|
||||
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("TRY_CALL - pushes value if variable exists but is not a function", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", 99],
|
||||
["STORE", "myVar"],
|
||||
["TRY_CALL", "myVar"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'number', value: 99 })
|
||||
})
|
||||
|
||||
test("TRY_CALL - pushes string if variable not found", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["TRY_CALL", "unknownVar"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'string', value: 'unknownVar' })
|
||||
})
|
||||
|
||||
test("TRY_CALL - handles arrays", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", 1],
|
||||
["PUSH", 2],
|
||||
["MAKE_ARRAY", 2],
|
||||
["STORE", "myArray"],
|
||||
["TRY_CALL", "myArray"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result.type).toBe('array')
|
||||
if (result.type === 'array') {
|
||||
expect(result.value).toEqual([
|
||||
{ type: 'number', value: 1 },
|
||||
{ type: 'number', value: 2 }
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
test("TRY_CALL - handles dicts", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", "key"],
|
||||
["PUSH", "value"],
|
||||
["MAKE_DICT", 1],
|
||||
["STORE", "myDict"],
|
||||
["TRY_CALL", "myDict"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result.type).toBe('dict')
|
||||
if (result.type === 'dict') {
|
||||
expect(result.value.get('key')).toEqual({ type: 'string', value: 'value' })
|
||||
}
|
||||
})
|
||||
|
||||
test("TRY_CALL - handles null values", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", null],
|
||||
["STORE", "myNull"],
|
||||
["TRY_CALL", "myNull"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'null', value: null })
|
||||
})
|
||||
|
||||
test("TRY_CALL - function can access its parameters", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["MAKE_FUNCTION", ["x"], ".body"],
|
||||
["STORE", "addFive"],
|
||||
["PUSH", 10],
|
||||
["STORE", "x"],
|
||||
["TRY_CALL", "addFive"],
|
||||
["HALT"],
|
||||
[".body:"],
|
||||
["LOAD", "x"],
|
||||
["PUSH", 5],
|
||||
["ADD"],
|
||||
["RETURN"]
|
||||
])
|
||||
|
||||
const result = await new VM(bytecode).run()
|
||||
// Function is called with 0 args, so x inside function should be null
|
||||
// Then we add 5 to null (which coerces to 0)
|
||||
expect(result).toEqual({ type: 'number', value: 5 })
|
||||
})
|
||||
|
||||
test("TRY_CALL - with string format", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
MAKE_FUNCTION () #4
|
||||
STORE myFunc
|
||||
TRY_CALL myFunc
|
||||
HALT
|
||||
PUSH 100
|
||||
RETURN
|
||||
`)
|
||||
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'number', value: 100 })
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user