tmp change - LOAD_NATIVE

This commit is contained in:
Chris Wanstrath 2025-10-17 12:35:38 -07:00
parent 62f890e59d
commit 4d2ae1c9fe
8 changed files with 178 additions and 63 deletions

View File

@ -74,7 +74,7 @@ type InstructionTuple =
| ["STR_CONCAT", number]
// Native
| ["CALL_NATIVE", string]
| ["LOAD_NATIVE", string]
// Special
| ["HALT"]
@ -88,7 +88,7 @@ export type ProgramItem = InstructionTuple | LabelDefinition
// Operand types are determined by prefix/literal:
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
// name -> variable/function name (e.g., LOAD x, CALL_NATIVE add)
// name -> variable/function name (e.g., LOAD x, LOAD_NATIVE add)
// 42 -> number constant (e.g., PUSH 42)
// "str" -> string constant (e.g., PUSH "hello")
// 'str' -> string constant (e.g., PUSH 'hello')
@ -336,7 +336,7 @@ function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
case "STORE":
case "TRY_LOAD":
case "TRY_CALL":
case "CALL_NATIVE":
case "LOAD_NATIVE":
operandValue = operand as string
break

View File

@ -66,7 +66,7 @@ export enum OpCode {
STR_CONCAT, // operand: value count (number) | stack: [val1, ..., valN] → [string] | concatenate N values
// typescript interop
CALL_NATIVE, // operand: function name (identifier) | stack: [...args] → [result] | consumes entire stack
LOAD_NATIVE, // operand: function name (identifier) | stack: [] → [function] | load native function
// special
HALT // operand: none | stop execution

View File

@ -45,7 +45,7 @@ const OPCODES_WITH_OPERANDS = new Set([
OpCode.MAKE_DICT,
OpCode.STR_CONCAT,
OpCode.MAKE_FUNCTION,
OpCode.CALL_NATIVE,
OpCode.LOAD_NATIVE,
])
const OPCODES_WITHOUT_OPERANDS = new Set([

View File

@ -1,5 +1,7 @@
import { Scope } from "./scope"
type NativeFunction = (...args: Value[]) => Promise<Value> | Value
export type Value =
| { type: 'null', value: null }
| { type: 'boolean', value: boolean }
@ -18,6 +20,7 @@ export type Value =
named: boolean,
value: '<function>'
}
| { type: 'native_function', fn: NativeFunction, value: '<native>' }
export type Dict = Map<string, Value>
@ -101,6 +104,8 @@ export function toString(v: Value): string {
return 'null'
case 'function':
return '<function>'
case 'native_function':
return '<native>'
case 'array':
return `[${v.value.map(toString).join(', ')}]`
case 'dict': {
@ -145,6 +150,8 @@ export function isEqual(a: Value, b: Value): boolean {
}
case 'function':
return false // functions never equal
case 'native_function':
return false // native functions never equal
default:
return false
}
@ -168,6 +175,8 @@ export function fromValue(v: Value): any {
return v.value
case 'function':
return '<function>'
case 'native_function':
return '<native>'
}
}

View File

@ -430,6 +430,21 @@ export class VM {
const fn = this.stack.pop()!
// Handle native functions
if (fn.type === 'native_function') {
if (namedCount > 0)
throw new Error('CALL: native functions do not support named arguments')
// Mark current frame as break target (like regular CALL does)
if (this.callStack.length > 0)
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
// Call the native function with positional args
const result = await fn.fn(...positionalArgs)
this.stack.push(result)
break
}
if (fn.type !== 'function')
throw new Error('CALL: not a function')
@ -591,27 +606,16 @@ export class VM {
this.stack.push(returnValue)
break
case OpCode.CALL_NATIVE:
case OpCode.LOAD_NATIVE: {
const functionName = instruction.operand as string
const tsFunction = this.nativeFunctions.get(functionName)
const nativeFunc = this.nativeFunctions.get(functionName)
if (!tsFunction)
throw new Error(`CALL_NATIVE: function not found: ${functionName}`)
if (!nativeFunc)
throw new Error(`LOAD_NATIVE: function not found: ${functionName}`)
// Mark current frame as break target (like CALL does)
if (this.callStack.length > 0)
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
// Pop all arguments from stack (TypeScript function consumes entire stack)
const tsArgs = [...this.stack]
this.stack = []
// Call the TypeScript function and await if necessary
const tsResult = await tsFunction(...tsArgs)
// Push result back onto stack
this.stack.push(tsResult)
this.stack.push({ type: 'native_function', fn: nativeFunc, value: '<native>' })
break
}
default:
throw `Unknown op: ${instruction.op}`

View File

@ -5,9 +5,12 @@ import { toBytecode } from "#bytecode"
describe("functions parameter", () => {
test("pass functions to run()", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE add
PUSH 5
PUSH 3
CALL_NATIVE add
PUSH 2
PUSH 0
CALL
HALT
`)
@ -20,9 +23,12 @@ describe("functions parameter", () => {
test("pass functions to VM constructor", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE multiply
PUSH 10
PUSH 2
CALL_NATIVE multiply
PUSH 2
PUSH 0
CALL
HALT
`)
@ -36,11 +42,19 @@ describe("functions parameter", () => {
test("pass multiple functions", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE add
PUSH 10
PUSH 5
CALL_NATIVE add
PUSH 2
PUSH 0
CALL
STORE sum
LOAD_NATIVE multiply
LOAD sum
PUSH 3
CALL_NATIVE multiply
PUSH 2
PUSH 0
CALL
HALT
`)
@ -54,9 +68,12 @@ describe("functions parameter", () => {
test("auto-wraps native functions", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE concat
PUSH "hello"
PUSH "world"
CALL_NATIVE concat
PUSH 2
PUSH 0
CALL
HALT
`)
@ -69,8 +86,11 @@ describe("functions parameter", () => {
test("works with async functions", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE delay
PUSH 100
CALL_NATIVE delay
PUSH 1
PUSH 0
CALL
HALT
`)
@ -86,11 +106,19 @@ describe("functions parameter", () => {
test("can combine with manual registerFunction", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE add
PUSH 5
PUSH 3
CALL_NATIVE add
PUSH 2
CALL_NATIVE subtract
PUSH 0
CALL
STORE sum
LOAD_NATIVE subtract
LOAD sum
PUSH 2
PUSH 2
PUSH 0
CALL
HALT
`)
@ -127,8 +155,11 @@ describe("functions parameter", () => {
test("function throws error", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE divide
PUSH 0
CALL_NATIVE divide
PUSH 1
PUSH 0
CALL
HALT
`)
@ -147,16 +178,25 @@ describe("functions parameter", () => {
test("complex workflow with multiple function calls", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE add
PUSH 5
PUSH 3
CALL_NATIVE add
PUSH 2
PUSH 0
CALL
STORE result
LOAD_NATIVE multiply
LOAD result
PUSH 2
CALL_NATIVE multiply
PUSH 2
PUSH 0
CALL
STORE final
LOAD_NATIVE format
LOAD final
CALL_NATIVE format
PUSH 1
PUSH 0
CALL
HALT
`)
@ -171,8 +211,11 @@ describe("functions parameter", () => {
test("function overriding - later registration wins", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE getValue
PUSH 5
CALL_NATIVE getValue
PUSH 1
PUSH 0
CALL
HALT
`)

View File

@ -3,11 +3,14 @@ import { VM } from "#vm"
import { toBytecode } from "#bytecode"
import { toValue, toNumber, toString } from "#value"
test("CALL_NATIVE - basic function call", async () => {
test("LOAD_NATIVE - basic function call", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE add
PUSH 5
PUSH 10
CALL_NATIVE add
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -21,11 +24,14 @@ test("CALL_NATIVE - basic function call", async () => {
expect(result).toEqual({ type: 'number', value: 15 })
})
test("CALL_NATIVE - function with string manipulation", async () => {
test("LOAD_NATIVE - function with string manipulation", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE concat
PUSH "hello"
PUSH "world"
CALL_NATIVE concat
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -40,10 +46,13 @@ test("CALL_NATIVE - function with string manipulation", async () => {
expect(result).toEqual({ type: 'string', value: 'hello world' })
})
test("CALL_NATIVE - async function", async () => {
test("LOAD_NATIVE - async function", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE asyncDouble
PUSH 42
CALL_NATIVE asyncDouble
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -58,9 +67,12 @@ test("CALL_NATIVE - async function", async () => {
expect(result).toEqual({ type: 'number', value: 84 })
})
test("CALL_NATIVE - function with no arguments", async () => {
test("LOAD_NATIVE - function with no arguments", async () => {
const bytecode = toBytecode(`
CALL_NATIVE getAnswer
LOAD_NATIVE getAnswer
PUSH 0
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -73,12 +85,15 @@ test("CALL_NATIVE - function with no arguments", async () => {
expect(result).toEqual({ type: 'number', value: 42 })
})
test("CALL_NATIVE - function with multiple arguments", async () => {
test("LOAD_NATIVE - function with multiple arguments", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE sum
PUSH 2
PUSH 3
PUSH 4
CALL_NATIVE sum
PUSH 3
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -92,10 +107,13 @@ test("CALL_NATIVE - function with multiple arguments", async () => {
expect(result).toEqual({ type: 'number', value: 9 })
})
test("CALL_NATIVE - function returns array", async () => {
test("LOAD_NATIVE - function returns array", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE makeRange
PUSH 3
CALL_NATIVE makeRange
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -121,20 +139,26 @@ test("CALL_NATIVE - function returns array", async () => {
}
})
test("CALL_NATIVE - function not found", async () => {
test("LOAD_NATIVE - function not found", async () => {
const bytecode = toBytecode(`
CALL_NATIVE nonexistent
LOAD_NATIVE nonexistent
PUSH 0
PUSH 0
CALL
`)
const vm = new VM(bytecode)
expect(vm.run()).rejects.toThrow('CALL_NATIVE: function not found: nonexistent')
expect(vm.run()).rejects.toThrow('LOAD_NATIVE: function not found: nonexistent')
})
test("CALL_NATIVE - using result in subsequent operations", async () => {
test("LOAD_NATIVE - using result in subsequent operations", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE triple
PUSH 5
CALL_NATIVE triple
PUSH 1
PUSH 0
CALL
PUSH 10
ADD
`)
@ -151,9 +175,12 @@ test("CALL_NATIVE - using result in subsequent operations", async () => {
test("Native function wrapping - basic sync function with native types", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE add
PUSH 5
PUSH 10
CALL_NATIVE add
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -169,8 +196,11 @@ test("Native function wrapping - basic sync function with native types", async (
test("Native function wrapping - async function with native types", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE asyncDouble
PUSH 42
CALL_NATIVE asyncDouble
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -187,9 +217,12 @@ test("Native function wrapping - async function with native types", async () =>
test("Native function wrapping - string manipulation", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE concat
PUSH "hello"
PUSH "world"
CALL_NATIVE concat
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -205,8 +238,11 @@ test("Native function wrapping - string manipulation", async () => {
test("Native function wrapping - with default parameters", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE ls
PUSH "/home/user"
CALL_NATIVE ls
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -222,8 +258,11 @@ test("Native function wrapping - with default parameters", async () => {
test("Native function wrapping - returns array", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE makeRange
PUSH 3
CALL_NATIVE makeRange
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -247,9 +286,12 @@ test("Native function wrapping - returns array", async () => {
test("Native function wrapping - returns object (becomes dict)", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE makeUser
PUSH "Alice"
PUSH 30
CALL_NATIVE makeUser
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
@ -269,9 +311,17 @@ test("Native function wrapping - returns object (becomes dict)", async () => {
test("Native function wrapping - mixed with manual Value functions", async () => {
const bytecode = toBytecode(`
LOAD_NATIVE nativeAdd
PUSH 5
CALL_NATIVE nativeAdd
CALL_NATIVE manualDouble
PUSH 1
PUSH 0
CALL
STORE sum
LOAD_NATIVE manualDouble
LOAD sum
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)

View File

@ -387,9 +387,12 @@ describe("RegExp", () => {
test("with native functions", async () => {
const { VM } = await import("#vm")
const bytecode = toBytecode(`
LOAD_NATIVE match
PUSH "hello world"
PUSH /world/
CALL_NATIVE match
PUSH 2
PUSH 0
CALL
HALT
`)
@ -407,10 +410,13 @@ describe("RegExp", () => {
test("native function with regex replacement", async () => {
const { VM } = await import("#vm")
const bytecode = toBytecode(`
LOAD_NATIVE replace
PUSH "hello world"
PUSH /o/g
PUSH "0"
CALL_NATIVE replace
PUSH 3
PUSH 0
CALL
HALT
`)
@ -427,9 +433,12 @@ describe("RegExp", () => {
test("native function extracting matches", async () => {
const { VM } = await import("#vm")
const bytecode = toBytecode(`
LOAD_NATIVE extractNumbers
PUSH "test123abc456"
PUSH /\\d+/g
CALL_NATIVE extractNumbers
PUSH 2
PUSH 0
CALL
HALT
`)