diff --git a/CLAUDE.md b/CLAUDE.md index aea859e..fa94a26 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -136,15 +136,50 @@ Array format features: - See `tests/programmatic.test.ts` and `examples/programmatic.ts` for examples ### Native Function Registration + +ReefVM supports two ways to register native functions: + +**1. Native TypeScript functions (recommended)** - Auto-converts between native TS and ReefVM types: ```typescript const vm = new VM(bytecode) -vm.registerFunction('functionName', (...args: Value[]): Value => { - // Implementation - return toValue(result) + +// Works with native TypeScript types! +vm.registerFunction('add', (a: number, b: number) => { + return a + b }) + +// Supports defaults (like NOSE commands) +vm.registerFunction('ls', (path: string, link = false) => { + return link ? `listing ${path} with links` : `listing ${path}` +}) + +// Async functions work too +vm.registerFunction('fetch', async (url: string) => { + const response = await fetch(url) + return await response.text() +}) + await vm.run() ``` +**2. Value-based functions (manual)** - For functions that need direct Value access: +```typescript +const vm = new VM(bytecode) + +vm.registerValueFunction('customOp', (a: Value, b: Value): Value => { + // Direct access to Value types + return toValue(toNumber(a) + toNumber(b)) +}) + +await vm.run() +``` + +The auto-wrapping handles: +- Converting Value → native types on input (using `fromValue`) +- Converting native types → Value on output (using `toValue`) +- Both sync and async functions +- Arrays, objects, primitives, and null + ### Label Usage (Preferred) Use labels instead of numeric offsets for readability: ``` diff --git a/README.md b/README.md index 8d1285c..0505b5c 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Commands: `clear`, `reset`, `exit`. - 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 - Native function interop (CALL_NATIVE) with sync and async functions +- **Auto-wrapping native functions** - register functions with native TypeScript types instead of Value types ## Design Decisions diff --git a/src/index.ts b/src/index.ts index 26308d7..55e3b3e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,5 +8,5 @@ export async function run(bytecode: Bytecode): Promise { } export { type Bytecode, toBytecode } from "./bytecode" -export { type Value, toValue, toString, toNumber, toJs, toNull } from "./value" +export { type Value, toValue, toString, toNumber, fromValue as toJs, fromValue, toNull, wrapNative } from "./value" export { VM } from "./vm" \ No newline at end of file diff --git a/src/value.ts b/src/value.ts index f71410b..e51368f 100644 --- a/src/value.ts +++ b/src/value.ts @@ -33,6 +33,9 @@ export function toValue(v: any): Value /* throws */ { if (v === null || v === undefined) return { type: 'null', value: null } + if (v && typeof v === 'object' && 'type' in v && 'value' in v) + return v as Value + if (Array.isArray(v)) return { type: 'array', value: v.map(toValue) } @@ -48,7 +51,7 @@ export function toValue(v: any): Value /* throws */ { case 'object': const dict: Dict = new Map() - for (const key in Object.keys(v)) + for (const key of Object.keys(v)) dict.set(key, toValue(v[key])) return { type: 'dict', value: dict } @@ -122,18 +125,37 @@ export function isEqual(a: Value, b: Value): boolean { } } -export function toJs(v: Value): any { +export function fromValue(v: Value): any { switch (v.type) { case 'null': return null case 'boolean': return v.value case 'number': return v.value case 'string': return v.value - case 'array': return v.value.map(toJs) - case 'dict': return Object.fromEntries(v.value.entries().map(([k, v]) => [k, toJs(v)])) + case 'array': return v.value.map(fromValue) + case 'dict': return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)])) case 'function': return '' } } export function toNull(): Value { return toValue(null) +} + +const WRAPPED_MARKER = Symbol('reef-wrapped') + +export function wrapNative(fn: Function): (...args: Value[]) => Promise { + const wrapped = async (...values: Value[]) => { + const nativeArgs = values.map(fromValue) + const result = await fn(...nativeArgs) + return toValue(result) + } + + const wrappedObj = wrapped as any + wrappedObj[WRAPPED_MARKER] = true + + return wrapped +} + +export function isWrapped(fn: Function): boolean { + return !!(fn as any)[WRAPPED_MARKER] } \ No newline at end of file diff --git a/src/vm.ts b/src/vm.ts index 6442f39..f2dacba 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -3,7 +3,7 @@ import type { ExceptionHandler } from "./exception" import { type Frame } from "./frame" import { OpCode } from "./opcode" import { Scope } from "./scope" -import { type Value, toValue, toNumber, isTrue, isEqual, toString } from "./value" +import { type Value, toValue, toNumber, isTrue, isEqual, toString, wrapNative, isWrapped } from "./value" type NativeFunction = (...args: Value[]) => Promise | Value @@ -26,7 +26,14 @@ export class VM { this.scope = new Scope() } - registerFunction(name: string, fn: NativeFunction) { + registerFunction(name: string, fn: NativeFunction | Function) { + // If it's already a NativeFunction, use it directly + // Otherwise, assume it's a JS function and wrap it + const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(fn) + this.nativeFunctions.set(name, wrapped) + } + + registerValueFunction(name: string, fn: NativeFunction) { this.nativeFunctions.set(name, fn) } diff --git a/tests/native.test.ts b/tests/native.test.ts index cc6607a..d14ad5c 100644 --- a/tests/native.test.ts +++ b/tests/native.test.ts @@ -12,8 +12,8 @@ test("CALL_NATIVE - basic function call", async () => { const vm = new VM(bytecode) - // Register a native function - vm.registerFunction('add', (a, b) => { + // Register a Value-based function + vm.registerValueFunction('add', (a, b) => { return toValue(toNumber(a) + toNumber(b)) }) @@ -30,7 +30,7 @@ test("CALL_NATIVE - function with string manipulation", async () => { const vm = new VM(bytecode) - vm.registerFunction('concat', (a, b) => { + vm.registerValueFunction('concat', (a, b) => { const aStr = a.type === 'string' ? a.value : toString(a) const bStr = b.type === 'string' ? b.value : toString(b) return toValue(aStr + ' ' + bStr) @@ -48,7 +48,7 @@ test("CALL_NATIVE - async function", async () => { const vm = new VM(bytecode) - vm.registerFunction('asyncDouble', async (a) => { + vm.registerValueFunction('asyncDouble', async (a) => { // Simulate async operation await new Promise(resolve => setTimeout(resolve, 1)) return toValue(toNumber(a) * 2) @@ -65,7 +65,7 @@ test("CALL_NATIVE - function with no arguments", async () => { const vm = new VM(bytecode) - vm.registerFunction('getAnswer', () => { + vm.registerValueFunction('getAnswer', () => { return toValue(42) }) @@ -83,7 +83,7 @@ test("CALL_NATIVE - function with multiple arguments", async () => { const vm = new VM(bytecode) - vm.registerFunction('sum', (...args) => { + vm.registerValueFunction('sum', (...args) => { const total = args.reduce((acc, val) => acc + toNumber(val), 0) return toValue(total) }) @@ -100,7 +100,7 @@ test("CALL_NATIVE - function returns array", async () => { const vm = new VM(bytecode) - vm.registerFunction('makeRange', (n) => { + vm.registerValueFunction('makeRange', (n) => { const count = toNumber(n) const arr = [] for (let i = 0; i < count; i++) { @@ -141,10 +141,149 @@ test("CALL_NATIVE - using result in subsequent operations", async () => { const vm = new VM(bytecode) - vm.registerFunction('triple', (n) => { + vm.registerValueFunction('triple', (n) => { return toValue(toNumber(n) * 3) }) const result = await vm.run() expect(result).toEqual({ type: 'number', value: 25 }) }) + +test("Native function wrapping - basic sync function with native types", async () => { + const bytecode = toBytecode(` + PUSH 5 + PUSH 10 + CALL_NATIVE add + `) + + const vm = new VM(bytecode) + + // Register with native TypeScript types - auto-wraps! + vm.registerFunction('add', (a: number, b: number) => { + return a + b + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 15 }) +}) + +test("Native function wrapping - async function with native types", async () => { + const bytecode = toBytecode(` + PUSH 42 + CALL_NATIVE asyncDouble + `) + + const vm = new VM(bytecode) + + // Async native function + vm.registerFunction('asyncDouble', async (n: number) => { + await new Promise(resolve => setTimeout(resolve, 1)) + return n * 2 + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 84 }) +}) + +test("Native function wrapping - string manipulation", async () => { + const bytecode = toBytecode(` + PUSH "hello" + PUSH "world" + CALL_NATIVE concat + `) + + const vm = new VM(bytecode) + + // Native string function + vm.registerFunction('concat', (a: string, b: string) => { + return a + ' ' + b + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'string', value: 'hello world' }) +}) + +test("Native function wrapping - with default parameters", async () => { + const bytecode = toBytecode(` + PUSH "/home/user" + CALL_NATIVE ls + `) + + const vm = new VM(bytecode) + + // Function with default parameter (like NOSE commands) + vm.registerFunction('ls', (path: string, link = false) => { + return link ? `listing ${path} with links` : `listing ${path}` + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'string', value: 'listing /home/user' }) +}) + +test("Native function wrapping - returns array", async () => { + const bytecode = toBytecode(` + PUSH 3 + CALL_NATIVE makeRange + `) + + const vm = new VM(bytecode) + + // Return native array - auto-converts to Value array + vm.registerFunction('makeRange', (n: number) => { + return Array.from({ length: n }, (_, i) => i) + }) + + const result = await vm.run() + expect(result.type).toBe('array') + if (result.type === 'array') { + expect(result.value.length).toBe(3) + expect(result.value).toEqual([ + toValue(0), + toValue(1), + toValue(2) + ]) + } +}) + +test("Native function wrapping - returns object (becomes dict)", async () => { + const bytecode = toBytecode(` + PUSH "Alice" + PUSH 30 + CALL_NATIVE makeUser + `) + + const vm = new VM(bytecode) + + // Return plain object - auto-converts to dict + vm.registerFunction('makeUser', (name: string, age: number) => { + return { name, age } + }) + + const result = await vm.run() + expect(result.type).toBe('dict') + if (result.type === 'dict') { + expect(result.value.get('name')).toEqual(toValue('Alice')) + expect(result.value.get('age')).toEqual(toValue(30)) + } +}) + +test("Native function wrapping - mixed with manual Value functions", async () => { + const bytecode = toBytecode(` + PUSH 5 + CALL_NATIVE nativeAdd + CALL_NATIVE manualDouble + `) + + const vm = new VM(bytecode) + + // Native function (auto-wrapped by registerFunction) + vm.registerFunction('nativeAdd', (n: number) => n + 10) + + // Manual Value function (use registerValueFunction) + vm.registerValueFunction('manualDouble', (v) => { + return toValue(toNumber(v) * 2) + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 30 }) +})