diff --git a/CLAUDE.md b/CLAUDE.md index fa94a26..4550f7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,14 +75,17 @@ No build step required - Bun runs TypeScript directly. ## Testing Strategy Tests are organized by feature area: -- **basic.test.ts**: Stack ops, arithmetic, comparisons, variables, control flow +- **opcodes.test.ts**: Stack ops, arithmetic, comparisons, variables, control flow - **functions.test.ts**: Function creation, calls, closures, defaults, variadic, named args - **tail-call.test.ts**: Tail call optimization and unbounded recursion - **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers - **native.test.ts**: Native function interop (sync and async) +- **functions-parameter.test.ts**: Convenience parameter for passing functions to run() and VM - **bytecode.test.ts**: Bytecode string parser, label resolution, constants - **programmatic.test.ts**: Array format API, typed tuples, labels, functions - **validator.test.ts**: Bytecode validation rules +- **unicode.test.ts**: Unicode and emoji identifiers +- **regex.test.ts**: RegExp support - **examples.test.ts**: Integration tests for example programs When adding features: @@ -137,48 +140,35 @@ Array format features: ### Native Function Registration -ReefVM supports two ways to register native functions: +**Option 1**: Pass to `run()` or `VM` constructor (convenience) +```typescript +const result = await run(bytecode, { + add: (a: number, b: number) => a + b, + greet: (name: string) => `Hello, ${name}!` +}) -**1. Native TypeScript functions (recommended)** - Auto-converts between native TS and ReefVM types: +// Or with VM constructor +const vm = new VM(bytecode, { add, greet }) +``` + +**Option 2**: Register with `vm.registerFunction()` (manual) ```typescript const vm = new VM(bytecode) - -// 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() -}) - +vm.registerFunction('add', (a: number, b: number) => a + b) await vm.run() ``` -**2. Value-based functions (manual)** - For functions that need direct Value access: +**Option 3**: Register Value-based functions (for 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 +Auto-wrapping handles: +- Value ↔ native type conversion (`fromValue`/`toValue`) +- Sync and async functions +- Arrays, objects, primitives, null, RegExp ### Label Usage (Preferred) Use labels instead of numeric offsets for readability: diff --git a/GUIDE.md b/GUIDE.md index 850102a..9ff5021 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -547,6 +547,28 @@ All calls push arguments in order: ### CALL_NATIVE Behavior Unlike CALL, CALL_NATIVE consumes the **entire stack** as arguments and clears the stack. The native function receives all values that were on the stack at the time of the call. +### Registering Native Functions + +**Method 1**: Pass to `run()` or `VM` constructor +```typescript +const result = await run(bytecode, { + add: (a: number, b: number) => a + b, + greet: (name: string) => `Hello, ${name}!` +}) + +// Or with VM +const vm = new VM(bytecode, { add, greet }) +``` + +**Method 2**: Register manually +```typescript +const vm = new VM(bytecode) +vm.registerFunction('add', (a, b) => a + b) +await vm.run() +``` + +Functions are auto-wrapped to convert between native TypeScript and ReefVM Value types. Both sync and async functions work. + ### Empty Stack - RETURN with empty stack returns null - HALT with empty stack returns null diff --git a/README.md b/README.md index 30e24a1..ff07af5 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ Commands: `clear`, `reset`, `exit`. - Mixed positional and named arguments with proper priority binding - 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 -- Write native functions with regular TypeScript types instead of Shrimp's internal Value types +- Native function interop (CALL_NATIVE) with auto-wrapping for native TypeScript types +- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })` ## Design Decisions diff --git a/src/index.ts b/src/index.ts index b2606cd..a8b17e3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,8 @@ import type { Bytecode } from "./bytecode" import { type Value } from "./value" import { VM } from "./vm" -export async function run(bytecode: Bytecode): Promise { - const vm = new VM(bytecode) +export async function run(bytecode: Bytecode, functions?: Record): Promise { + const vm = new VM(bytecode, functions) return await vm.run() } diff --git a/src/vm.ts b/src/vm.ts index 1620571..98fa816 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -19,11 +19,15 @@ export class VM { labels: Map = new Map() nativeFunctions: Map = new Map() - constructor(bytecode: Bytecode) { + constructor(bytecode: Bytecode, functions?: Record) { this.instructions = bytecode.instructions this.constants = bytecode.constants this.labels = bytecode.labels || new Map() this.scope = new Scope() + + if (functions) + for (const name of Object.keys(functions)) + this.registerFunction(name, functions[name]!) } registerFunction(name: string, fn: Function) { diff --git a/tests/functions-parameter.test.ts b/tests/functions-parameter.test.ts new file mode 100644 index 0000000..f5f0bf7 --- /dev/null +++ b/tests/functions-parameter.test.ts @@ -0,0 +1,189 @@ +import { test, expect, describe } from "bun:test" +import { run, VM } from "#index" +import { toBytecode } from "#bytecode" + +describe("functions parameter", () => { + test("pass functions to run()", async () => { + const bytecode = toBytecode(` + PUSH 5 + PUSH 3 + CALL_NATIVE add + HALT + `) + + const result = await run(bytecode, { + add: (a: number, b: number) => a + b + }) + + expect(result).toEqual({ type: 'number', value: 8 }) + }) + + test("pass functions to VM constructor", async () => { + const bytecode = toBytecode(` + PUSH 10 + PUSH 2 + CALL_NATIVE multiply + HALT + `) + + const vm = new VM(bytecode, { + multiply: (a: number, b: number) => a * b + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 20 }) + }) + + test("pass multiple functions", async () => { + const bytecode = toBytecode(` + PUSH 10 + PUSH 5 + CALL_NATIVE add + PUSH 3 + CALL_NATIVE multiply + HALT + `) + + const result = await run(bytecode, { + add: (a: number, b: number) => a + b, + multiply: (a: number, b: number) => a * b + }) + + expect(result).toEqual({ type: 'number', value: 45 }) + }) + + test("auto-wraps native functions", async () => { + const bytecode = toBytecode(` + PUSH "hello" + PUSH "world" + CALL_NATIVE concat + HALT + `) + + const result = await run(bytecode, { + concat: (a: string, b: string) => a + " " + b + }) + + expect(result).toEqual({ type: 'string', value: 'hello world' }) + }) + + test("works with async functions", async () => { + const bytecode = toBytecode(` + PUSH 100 + CALL_NATIVE delay + HALT + `) + + const result = await run(bytecode, { + delay: async (n: number) => { + await new Promise(resolve => setTimeout(resolve, 1)) + return n * 2 + } + }) + + expect(result).toEqual({ type: 'number', value: 200 }) + }) + + test("can combine with manual registerFunction", async () => { + const bytecode = toBytecode(` + PUSH 5 + PUSH 3 + CALL_NATIVE add + PUSH 2 + CALL_NATIVE subtract + HALT + `) + + const vm = new VM(bytecode, { + add: (a: number, b: number) => a + b + }) + + // Register another function manually + vm.registerFunction('subtract', (a: number, b: number) => a - b) + + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 6 }) + }) + + test("no functions parameter (undefined)", async () => { + const bytecode = toBytecode(` + PUSH 42 + HALT + `) + + const result = await run(bytecode) + expect(result).toEqual({ type: 'number', value: 42 }) + }) + + test("empty functions object", async () => { + const bytecode = toBytecode(` + PUSH 99 + HALT + `) + + const result = await run(bytecode, {}) + expect(result).toEqual({ type: 'number', value: 99 }) + }) + + test("function throws error", async () => { + const bytecode = toBytecode(` + PUSH 0 + CALL_NATIVE divide + HALT + `) + + try { + await run(bytecode, { + divide: (n: number) => { + if (n === 0) throw new Error("Cannot divide by zero") + return 100 / n + } + }) + expect(true).toBe(false) // Should not reach here + } catch (e: any) { + expect(e.message).toContain("Cannot divide by zero") + } + }) + + test("complex workflow with multiple function calls", async () => { + const bytecode = toBytecode(` + PUSH 5 + PUSH 3 + CALL_NATIVE add + STORE result + LOAD result + PUSH 2 + CALL_NATIVE multiply + STORE final + LOAD final + CALL_NATIVE format + HALT + `) + + const result = await run(bytecode, { + add: (a: number, b: number) => a + b, + multiply: (a: number, b: number) => a * b, + format: (n: number) => `Result: ${n}` + }) + + expect(result).toEqual({ type: 'string', value: 'Result: 16' }) + }) + + test("function overriding - later registration wins", async () => { + const bytecode = toBytecode(` + PUSH 5 + CALL_NATIVE getValue + HALT + `) + + const vm = new VM(bytecode, { + getValue: () => 100 + }) + + // Override with manual registration + vm.registerFunction('getValue', () => 200) + + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 200 }) + }) +})