forked from defunkt/ReefVM
pass native functions to VM
This commit is contained in:
parent
23fcf05439
commit
b16351ac95
52
CLAUDE.md
52
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:
|
||||
|
|
|
|||
22
GUIDE.md
22
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Value> {
|
||||
const vm = new VM(bytecode)
|
||||
export async function run(bytecode: Bytecode, functions?: Record<string, Function>): Promise<Value> {
|
||||
const vm = new VM(bytecode, functions)
|
||||
return await vm.run()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,15 @@ export class VM {
|
|||
labels: Map<number, string> = new Map()
|
||||
nativeFunctions: Map<string, NativeFunction> = new Map()
|
||||
|
||||
constructor(bytecode: Bytecode) {
|
||||
constructor(bytecode: Bytecode, functions?: Record<string, Function>) {
|
||||
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) {
|
||||
|
|
|
|||
189
tests/functions-parameter.test.ts
Normal file
189
tests/functions-parameter.test.ts
Normal file
|
|
@ -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 })
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user