diff --git a/src/bytecode.ts b/src/bytecode.ts index 7824054..2d725f4 100644 --- a/src/bytecode.ts +++ b/src/bytecode.ts @@ -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 diff --git a/src/opcode.ts b/src/opcode.ts index 063da52..3a26d24 100644 --- a/src/opcode.ts +++ b/src/opcode.ts @@ -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 diff --git a/src/validator.ts b/src/validator.ts index 9766178..65753a3 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -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([ diff --git a/src/value.ts b/src/value.ts index 4edb9e4..6fb8653 100644 --- a/src/value.ts +++ b/src/value.ts @@ -1,5 +1,7 @@ import { Scope } from "./scope" +type NativeFunction = (...args: Value[]) => Promise | Value + export type Value = | { type: 'null', value: null } | { type: 'boolean', value: boolean } @@ -18,6 +20,7 @@ export type Value = named: boolean, value: '' } + | { type: 'native_function', fn: NativeFunction, value: '' } export type Dict = Map @@ -101,6 +104,8 @@ export function toString(v: Value): string { return 'null' case 'function': return '' + case 'native_function': + return '' 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 '' + case 'native_function': + return '' } } diff --git a/src/vm.ts b/src/vm.ts index 82bbb46..20c0164 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -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: '' }) break + } default: throw `Unknown op: ${instruction.op}` diff --git a/tests/functions-parameter.test.ts b/tests/functions-parameter.test.ts index f5f0bf7..a8208d0 100644 --- a/tests/functions-parameter.test.ts +++ b/tests/functions-parameter.test.ts @@ -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 `) diff --git a/tests/native.test.ts b/tests/native.test.ts index a4c980e..c6047cb 100644 --- a/tests/native.test.ts +++ b/tests/native.test.ts @@ -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) diff --git a/tests/regex.test.ts b/tests/regex.test.ts index 0ce1cfb..454ee09 100644 --- a/tests/regex.test.ts +++ b/tests/regex.test.ts @@ -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 `)