update bytecode text language

This commit is contained in:
Chris Wanstrath 2025-10-05 18:37:06 -07:00
parent e16a8104c7
commit 4608ec7b9e
7 changed files with 94 additions and 82 deletions

View File

@ -70,7 +70,7 @@ It's where Shrimp live.
- [x] DICT_HAS - [x] DICT_HAS
### TypeScript Interop ### TypeScript Interop
- [x] CALL_TYPESCRIPT - [x] CALL_NATIVE
### Special ### Special
- [x] HALT - [x] HALT
@ -88,7 +88,7 @@ It's where Shrimp live.
- All dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS) - All dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS)
- Basic function operations (MAKE_FUNCTION, CALL, RETURN) with parameter binding - Basic function operations (MAKE_FUNCTION, CALL, RETURN) with parameter binding
- 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
- TypeScript interop (CALL_TYPESCRIPT) with sync and async functions - TypeScript interop (CALL_NATIVE) with sync and async functions
- HALT instruction - HALT instruction
## Design Decisions ## Design Decisions

10
SPEC.md
View File

@ -16,7 +16,7 @@ The ReefVM is a stack-based bytecode virtual machine designed for the Shrimp pro
- **Scope Chain**: Linked scopes for lexical variable resolution - **Scope Chain**: Linked scopes for lexical variable resolution
- **Program Counter (PC)**: Current instruction index - **Program Counter (PC)**: Current instruction index
- **Constants Pool**: Immutable values and function metadata - **Constants Pool**: Immutable values and function metadata
- **TypeScript Function Registry**: External functions callable from Shrimp - **Native Function Registry**: External functions callable from Shrimp
### Execution Model ### Execution Model
@ -448,7 +448,7 @@ Key is coerced to string.
### TypeScript Interop ### TypeScript Interop
#### CALL_TYPESCRIPT #### CALL_NATIVE
**Operand**: Function name (string) **Operand**: Function name (string)
**Effect**: Call registered TypeScript function **Effect**: Call registered TypeScript function
**Stack**: [...args] → [returnValue] **Stack**: [...args] → [returnValue]
@ -456,7 +456,7 @@ Key is coerced to string.
**Behavior**: **Behavior**:
1. Look up function by name in registry 1. Look up function by name in registry
2. Mark current frame (if exists) as break target 2. Mark current frame (if exists) as break target
3. Await function call (TypeScript function receives arguments and returns a Value) 3. Await function call (native function receives arguments and returns a Value)
4. Push return value onto stack 4. Push return value onto stack
**Notes**: **Notes**:
@ -576,7 +576,7 @@ All of these should throw errors:
6. **Break Outside Loop**: BREAK with no break target 6. **Break Outside Loop**: BREAK with no break target
7. **Continue Outside Loop**: CONTINUE with no continue target 7. **Continue Outside Loop**: CONTINUE with no continue target
8. **Return Outside Function**: RETURN with no call frame 8. **Return Outside Function**: RETURN with no call frame
9. **Unknown Function**: CALL_TYPESCRIPT with unregistered function 9. **Unknown Function**: CALL_NATIVE with unregistered function
10. **Mismatched Handler**: POP_TRY with no handler 10. **Mismatched Handler**: POP_TRY with no handler
11. **Invalid Constant**: PUSH with invalid constant index 11. **Invalid Constant**: PUSH with invalid constant index
12. **Invalid Function Definition**: MAKE_FUNCTION with non-function_def constant 12. **Invalid Function Definition**: MAKE_FUNCTION with non-function_def constant
@ -670,7 +670,7 @@ const result = await vm.execute()
- PC increment happens after each instruction execution - PC increment happens after each instruction execution
- Jump instructions use relative offsets (added to current PC after increment) - Jump instructions use relative offsets (added to current PC after increment)
- All async operations (TypeScript functions) must be awaited - All async operations (native functions) must be awaited
- Arrays and dicts are mutable (pass by reference) - Arrays and dicts are mutable (pass by reference)
- Functions are immutable values - Functions are immutable values
- The VM is single-threaded (no concurrency primitives) - The VM is single-threaded (no concurrency primitives)

View File

@ -15,10 +15,14 @@ export type Constant =
| Value | Value
| FunctionDef | FunctionDef
const opsWithVarNames = new Set([OpCode.LOAD, OpCode.STORE, OpCode.CALL_TYPESCRIPT]) //
const opsWithAddresses = new Set([OpCode.JUMP, OpCode.JUMP_IF_FALSE, OpCode.JUMP_IF_TRUE, OpCode.PUSH_TRY]) // Parse bytecode from human-readable string format.
const opsWithNumbers = new Set([OpCode.MAKE_ARRAY, OpCode.MAKE_DICT]) // Operand types are determined by prefix:
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
// name -> variable/function name (e.g., LOAD x, CALL_NATIVE add)
// 42 -> number constant (e.g., PUSH 42)
// "str" -> string constant (e.g., PUSH "hello")
// 'str' -> string constant (e.g., PUSH 'hello')
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")
@ -28,34 +32,42 @@ export function toBytecode(str: string): Bytecode /* throws */ {
} }
for (let line of lines) { for (let line of lines) {
let [op, operand] = line.trim().split(" ") const trimmed = line.trim()
if (!trimmed) continue
const [op, ...rest] = trimmed.split(/\s+/)
const opCode = OpCode[op as keyof typeof OpCode] const opCode = OpCode[op as keyof typeof OpCode]
if (opCode === undefined) {
throw new Error(`Unknown opcode: ${op}`)
}
let operandValue: number | string | undefined = undefined let operandValue: number | string | undefined = undefined
if (operand) { if (rest.length > 0) {
// Variable names for LOAD, STORE, CALL_TYPESCRIPT const operand = rest.join(' ')
if (opsWithVarNames.has(opCode)) {
operandValue = operand if (operand.startsWith('#')) {
} // immediate number
// Direct addresses for JUMP operations operandValue = parseInt(operand.slice(1))
else if (opsWithAddresses.has(opCode)) {
operandValue = parseInt(operand) } else if (/^['"].*['"]$/.test(operand)) {
} // string
// Direct numbers for MAKE_ARRAY, MAKE_DICT const stringValue = operand.slice(1, operand.length - 1)
else if (opsWithNumbers.has(opCode)) { bytecode.constants.push(toValue(stringValue))
operandValue = parseInt(operand)
}
// Constants (numbers, strings) for PUSH
else {
if (/^\d+/.test(operand)) {
bytecode.constants.push(toValue(parseFloat(operand)))
} else if (/^['"]/.test(operand)) {
bytecode.constants.push(toValue(operand.slice(1, operand.length - 1)))
} else {
throw `Unknown operand: ${operand}`
}
operandValue = bytecode.constants.length - 1 operandValue = bytecode.constants.length - 1
} else if (/^-?\d+(\.\d+)?$/.test(operand)) {
// number
bytecode.constants.push(toValue(parseFloat(operand)))
operandValue = bytecode.constants.length - 1
} else if (/^[a-zA-Z_].*$/.test(operand)) {
// variable
operandValue = operand
} else {
throw new Error(`Invalid operand: ${operand}`)
} }
} }

View File

@ -59,7 +59,7 @@ export enum OpCode {
DICT_HAS, DICT_HAS,
// typescript interop // typescript interop
CALL_TYPESCRIPT, CALL_NATIVE,
// special // special
HALT HALT

View File

@ -5,7 +5,7 @@ import { OpCode } from "./opcode"
import { Scope } from "./scope" import { Scope } from "./scope"
import { type Value, toValue, toNumber, isTrue, isEqual, toString } from "./value" import { type Value, toValue, toNumber, isTrue, isEqual, toString } from "./value"
type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value type NativeFunction = (...args: Value[]) => Promise<Value> | Value
export class VM { export class VM {
pc = 0 pc = 0
@ -16,7 +16,7 @@ export class VM {
scope: Scope scope: Scope
constants: Constant[] = [] constants: Constant[] = []
instructions: Instruction[] = [] instructions: Instruction[] = []
typescriptFunctions: Map<string, TypeScriptFunction> = new Map() nativeFunctions: Map<string, NativeFunction> = new Map()
constructor(bytecode: Bytecode) { constructor(bytecode: Bytecode) {
this.instructions = bytecode.instructions this.instructions = bytecode.instructions
@ -24,8 +24,8 @@ export class VM {
this.scope = new Scope() this.scope = new Scope()
} }
registerFunction(name: string, fn: TypeScriptFunction) { registerFunction(name: string, fn: NativeFunction) {
this.typescriptFunctions.set(name, fn) this.nativeFunctions.set(name, fn)
} }
async run(): Promise<Value> { async run(): Promise<Value> {
@ -412,12 +412,12 @@ export class VM {
this.stack.push(returnValue) this.stack.push(returnValue)
break break
case OpCode.CALL_TYPESCRIPT: case OpCode.CALL_NATIVE:
const functionName = instruction.operand as string const functionName = instruction.operand as string
const tsFunction = this.typescriptFunctions.get(functionName) const tsFunction = this.nativeFunctions.get(functionName)
if (!tsFunction) if (!tsFunction)
throw new Error(`CALL_TYPESCRIPT: function not found: ${functionName}`) throw new Error(`CALL_NATIVE: function not found: ${functionName}`)
// Mark current frame as break target (like CALL does) // Mark current frame as break target (like CALL does)
if (this.callStack.length > 0) if (this.callStack.length > 0)

View File

@ -188,7 +188,7 @@ test("AND pattern - short circuits when false", async () => {
PUSH 0 PUSH 0
EQ EQ
DUP DUP
JUMP_IF_FALSE 2 JUMP_IF_FALSE #2
POP POP
PUSH 999 PUSH 999
` `
@ -203,7 +203,7 @@ 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 #2
POP POP
PUSH 2 PUSH 2
` `
@ -214,7 +214,7 @@ 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 #2
POP POP
PUSH 2 PUSH 2
` `
@ -227,7 +227,7 @@ test("OR pattern - evaluates second when false", async () => {
PUSH 0 PUSH 0
EQ EQ
DUP DUP
JUMP_IF_TRUE 2 JUMP_IF_TRUE #2
POP POP
PUSH 2 PUSH 2
` `
@ -263,7 +263,7 @@ 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 #1
PUSH 1 PUSH 1
` `
expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 }) expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 })
@ -271,7 +271,7 @@ test("isTruthy - only null and false are falsy", async () => {
// 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 #1
PUSH 1 PUSH 1
` `
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 1 }) expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 1 })
@ -281,7 +281,7 @@ 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 #1
PUSH 999 PUSH 999
` `
expect(await run(toBytecode(str3))).toEqual({ type: 'number', value: 999 }) expect(await run(toBytecode(str3))).toEqual({ type: 'number', value: 999 })
@ -323,7 +323,7 @@ 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 #1
PUSH 100 PUSH 100
PUSH 2 PUSH 2
` `
@ -334,7 +334,7 @@ test("JUMP - backward offset demonstrates relative jumps", 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 #2
PUSH 200 PUSH 200
PUSH 300 PUSH 300
PUSH 400 PUSH 400
@ -347,7 +347,7 @@ 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 #1
PUSH 100 PUSH 100
PUSH 42 PUSH 42
` `
@ -357,7 +357,7 @@ 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 #1
PUSH 100 PUSH 100
` `
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 }) expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 })
@ -366,7 +366,7 @@ 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 #1
PUSH 100 PUSH 100
PUSH 42 PUSH 42
` `
@ -378,7 +378,7 @@ test("MAKE_ARRAY - creates array", async () => {
PUSH 10 PUSH 10
PUSH 20 PUSH 20
PUSH 30 PUSH 30
MAKE_ARRAY 3 MAKE_ARRAY #3
` `
const result = await run(toBytecode(str)) const result = await run(toBytecode(str))
expect(result.type).toBe('array') expect(result.type).toBe('array')
@ -395,7 +395,7 @@ test("ARRAY_GET - gets element", async () => {
PUSH 10 PUSH 10
PUSH 20 PUSH 20
PUSH 30 PUSH 30
MAKE_ARRAY 3 MAKE_ARRAY #3
PUSH 1 PUSH 1
ARRAY_GET ARRAY_GET
` `
@ -407,7 +407,7 @@ test("ARRAY_SET - sets element", async () => {
PUSH 10 PUSH 10
PUSH 20 PUSH 20
PUSH 30 PUSH 30
MAKE_ARRAY 3 MAKE_ARRAY #3
DUP DUP
PUSH 1 PUSH 1
PUSH 99 PUSH 99
@ -422,7 +422,7 @@ test("ARRAY_PUSH - appends to array", async () => {
const str = ` const str = `
PUSH 10 PUSH 10
PUSH 20 PUSH 20
MAKE_ARRAY 2 MAKE_ARRAY #2
DUP DUP
PUSH 30 PUSH 30
ARRAY_PUSH ARRAY_PUSH
@ -435,7 +435,7 @@ test("ARRAY_PUSH - mutates original array", async () => {
const str = ` const str = `
PUSH 10 PUSH 10
PUSH 20 PUSH 20
MAKE_ARRAY 2 MAKE_ARRAY #2
DUP DUP
PUSH 30 PUSH 30
ARRAY_PUSH ARRAY_PUSH
@ -450,7 +450,7 @@ test("ARRAY_LEN - gets length", async () => {
PUSH 10 PUSH 10
PUSH 20 PUSH 20
PUSH 30 PUSH 30
MAKE_ARRAY 3 MAKE_ARRAY #3
ARRAY_LEN ARRAY_LEN
` `
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 }) expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
@ -462,7 +462,7 @@ test("MAKE_DICT - creates dict", async () => {
PUSH 'Alice' PUSH 'Alice'
PUSH 'age' PUSH 'age'
PUSH 30 PUSH 30
MAKE_DICT 2 MAKE_DICT #2
` `
const result = await run(toBytecode(str)) const result = await run(toBytecode(str))
expect(result.type).toBe('dict') expect(result.type).toBe('dict')
@ -477,7 +477,7 @@ test("DICT_GET - gets value", async () => {
const str = ` const str = `
PUSH 'name' PUSH 'name'
PUSH 'Bob' PUSH 'Bob'
MAKE_DICT 1 MAKE_DICT #1
PUSH 'name' PUSH 'name'
DICT_GET DICT_GET
` `
@ -486,7 +486,7 @@ test("DICT_GET - gets value", async () => {
test("DICT_SET - sets value", async () => { test("DICT_SET - sets value", async () => {
const str = ` const str = `
MAKE_DICT 0 MAKE_DICT #0
DUP DUP
PUSH 'key' PUSH 'key'
PUSH 'value' PUSH 'value'
@ -501,7 +501,7 @@ test("DICT_HAS - checks key exists", async () => {
const str = ` const str = `
PUSH 'key' PUSH 'key'
PUSH 'value' PUSH 'value'
MAKE_DICT 1 MAKE_DICT #1
PUSH 'key' PUSH 'key'
DICT_HAS DICT_HAS
` `
@ -510,7 +510,7 @@ test("DICT_HAS - checks key exists", async () => {
test("DICT_HAS - checks key missing", async () => { test("DICT_HAS - checks key missing", async () => {
const str = ` const str = `
MAKE_DICT 0 MAKE_DICT #0
PUSH 'missing' PUSH 'missing'
DICT_HAS DICT_HAS
` `

View File

@ -3,12 +3,12 @@ import { VM } from "#vm"
import { OpCode } from "#opcode" import { OpCode } from "#opcode"
import { toValue, toNumber } from "#value" import { toValue, toNumber } from "#value"
test("CALL_TYPESCRIPT - basic function call", async () => { test("CALL_NATIVE - basic function call", async () => {
const vm = new VM({ const vm = new VM({
instructions: [ instructions: [
{ op: OpCode.PUSH, operand: 0 }, // push 5 { op: OpCode.PUSH, operand: 0 }, // push 5
{ op: OpCode.PUSH, operand: 1 }, // push 10 { op: OpCode.PUSH, operand: 1 }, // push 10
{ op: OpCode.CALL_TYPESCRIPT, operand: 'add' }, // call TypeScript 'add' { op: OpCode.CALL_NATIVE, operand: 'add' }, // call TypeScript 'add'
{ op: OpCode.HALT } { op: OpCode.HALT }
], ],
constants: [ constants: [
@ -26,12 +26,12 @@ test("CALL_TYPESCRIPT - basic function call", async () => {
expect(result).toEqual({ type: 'number', value: 15 }) expect(result).toEqual({ type: 'number', value: 15 })
}) })
test("CALL_TYPESCRIPT - function with string manipulation", async () => { test("CALL_NATIVE - function with string manipulation", async () => {
const vm = new VM({ const vm = new VM({
instructions: [ instructions: [
{ op: OpCode.PUSH, operand: 0 }, // push "hello" { op: OpCode.PUSH, operand: 0 }, // push "hello"
{ op: OpCode.PUSH, operand: 1 }, // push "world" { op: OpCode.PUSH, operand: 1 }, // push "world"
{ op: OpCode.CALL_TYPESCRIPT, operand: 'concat' }, // call TypeScript 'concat' { op: OpCode.CALL_NATIVE, operand: 'concat' }, // call TypeScript 'concat'
{ op: OpCode.HALT } { op: OpCode.HALT }
], ],
constants: [ constants: [
@ -50,11 +50,11 @@ test("CALL_TYPESCRIPT - function with string manipulation", async () => {
expect(result).toEqual({ type: 'string', value: 'hello world' }) expect(result).toEqual({ type: 'string', value: 'hello world' })
}) })
test("CALL_TYPESCRIPT - async function", async () => { test("CALL_NATIVE - async function", async () => {
const vm = new VM({ const vm = new VM({
instructions: [ instructions: [
{ op: OpCode.PUSH, operand: 0 }, // push 42 { op: OpCode.PUSH, operand: 0 }, // push 42
{ op: OpCode.CALL_TYPESCRIPT, operand: 'asyncDouble' }, // call async TypeScript function { op: OpCode.CALL_NATIVE, operand: 'asyncDouble' }, // call async TypeScript function
{ op: OpCode.HALT } { op: OpCode.HALT }
], ],
constants: [ constants: [
@ -72,10 +72,10 @@ test("CALL_TYPESCRIPT - async function", async () => {
expect(result).toEqual({ type: 'number', value: 84 }) expect(result).toEqual({ type: 'number', value: 84 })
}) })
test("CALL_TYPESCRIPT - function with no arguments", async () => { test("CALL_NATIVE - function with no arguments", async () => {
const vm = new VM({ const vm = new VM({
instructions: [ instructions: [
{ op: OpCode.CALL_TYPESCRIPT, operand: 'getAnswer' }, // call with empty stack { op: OpCode.CALL_NATIVE, operand: 'getAnswer' }, // call with empty stack
{ op: OpCode.HALT } { op: OpCode.HALT }
], ],
constants: [] constants: []
@ -89,13 +89,13 @@ test("CALL_TYPESCRIPT - function with no arguments", async () => {
expect(result).toEqual({ type: 'number', value: 42 }) expect(result).toEqual({ type: 'number', value: 42 })
}) })
test("CALL_TYPESCRIPT - function with multiple arguments", async () => { test("CALL_NATIVE - function with multiple arguments", async () => {
const vm = new VM({ const vm = new VM({
instructions: [ instructions: [
{ op: OpCode.PUSH, operand: 0 }, // push 2 { op: OpCode.PUSH, operand: 0 }, // push 2
{ op: OpCode.PUSH, operand: 1 }, // push 3 { op: OpCode.PUSH, operand: 1 }, // push 3
{ op: OpCode.PUSH, operand: 2 }, // push 4 { op: OpCode.PUSH, operand: 2 }, // push 4
{ op: OpCode.CALL_TYPESCRIPT, operand: 'sum' }, // call TypeScript 'sum' { op: OpCode.CALL_NATIVE, operand: 'sum' }, // call TypeScript 'sum'
{ op: OpCode.HALT } { op: OpCode.HALT }
], ],
constants: [ constants: [
@ -114,11 +114,11 @@ test("CALL_TYPESCRIPT - function with multiple arguments", async () => {
expect(result).toEqual({ type: 'number', value: 9 }) expect(result).toEqual({ type: 'number', value: 9 })
}) })
test("CALL_TYPESCRIPT - function returns array", async () => { test("CALL_NATIVE - function returns array", async () => {
const vm = new VM({ const vm = new VM({
instructions: [ instructions: [
{ op: OpCode.PUSH, operand: 0 }, // push 3 { op: OpCode.PUSH, operand: 0 }, // push 3
{ op: OpCode.CALL_TYPESCRIPT, operand: 'makeRange' }, // call TypeScript 'makeRange' { op: OpCode.CALL_NATIVE, operand: 'makeRange' }, // call TypeScript 'makeRange'
{ op: OpCode.HALT } { op: OpCode.HALT }
], ],
constants: [ constants: [
@ -147,22 +147,22 @@ test("CALL_TYPESCRIPT - function returns array", async () => {
} }
}) })
test("CALL_TYPESCRIPT - function not found", async () => { test("CALL_NATIVE - function not found", async () => {
const vm = new VM({ const vm = new VM({
instructions: [ instructions: [
{ op: OpCode.CALL_TYPESCRIPT, operand: 'nonexistent' } { op: OpCode.CALL_NATIVE, operand: 'nonexistent' }
], ],
constants: [] constants: []
}) })
await expect(vm.run()).rejects.toThrow('CALL_TYPESCRIPT: function not found: nonexistent') await expect(vm.run()).rejects.toThrow('CALL_NATIVE: function not found: nonexistent')
}) })
test("CALL_TYPESCRIPT - using result in subsequent operations", async () => { test("CALL_NATIVE - using result in subsequent operations", async () => {
const vm = new VM({ const vm = new VM({
instructions: [ instructions: [
{ op: OpCode.PUSH, operand: 0 }, // push 5 { op: OpCode.PUSH, operand: 0 }, // push 5
{ op: OpCode.CALL_TYPESCRIPT, operand: 'triple' }, // call TypeScript 'triple' -> 15 { op: OpCode.CALL_NATIVE, operand: 'triple' }, // call TypeScript 'triple' -> 15
{ op: OpCode.PUSH, operand: 1 }, // push 10 { op: OpCode.PUSH, operand: 1 }, // push 10
{ op: OpCode.ADD }, // 15 + 10 = 25 { op: OpCode.ADD }, // 15 + 10 = 25
{ op: OpCode.HALT } { op: OpCode.HALT }