more natural native functions

This commit is contained in:
Chris Wanstrath 2025-10-08 09:57:49 -07:00
parent 146b0a2883
commit 78923b3eff
6 changed files with 222 additions and 18 deletions

View File

@ -136,15 +136,50 @@ Array format features:
- See `tests/programmatic.test.ts` and `examples/programmatic.ts` for examples - See `tests/programmatic.test.ts` and `examples/programmatic.ts` for examples
### Native Function Registration ### 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 ```typescript
const vm = new VM(bytecode) const vm = new VM(bytecode)
vm.registerFunction('functionName', (...args: Value[]): Value => {
// Implementation // Works with native TypeScript types!
return toValue(result) 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() 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) ### Label Usage (Preferred)
Use labels instead of numeric offsets for readability: Use labels instead of numeric offsets for readability:
``` ```

View File

@ -47,6 +47,7 @@ Commands: `clear`, `reset`, `exit`.
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow) - 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 - 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 - 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 ## Design Decisions

View File

@ -8,5 +8,5 @@ export async function run(bytecode: Bytecode): Promise<Value> {
} }
export { type Bytecode, toBytecode } from "./bytecode" 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" export { VM } from "./vm"

View File

@ -33,6 +33,9 @@ export function toValue(v: any): Value /* throws */ {
if (v === null || v === undefined) if (v === null || v === undefined)
return { type: 'null', value: null } return { type: 'null', value: null }
if (v && typeof v === 'object' && 'type' in v && 'value' in v)
return v as Value
if (Array.isArray(v)) if (Array.isArray(v))
return { type: 'array', value: v.map(toValue) } return { type: 'array', value: v.map(toValue) }
@ -48,7 +51,7 @@ export function toValue(v: any): Value /* throws */ {
case 'object': case 'object':
const dict: Dict = new Map() 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])) dict.set(key, toValue(v[key]))
return { type: 'dict', value: dict } 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) { switch (v.type) {
case 'null': return null case 'null': return null
case 'boolean': return v.value case 'boolean': return v.value
case 'number': return v.value case 'number': return v.value
case 'string': return v.value case 'string': return v.value
case 'array': return v.value.map(toJs) case 'array': return v.value.map(fromValue)
case 'dict': return Object.fromEntries(v.value.entries().map(([k, v]) => [k, toJs(v)])) case 'dict': return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)]))
case 'function': return '<function>' case 'function': return '<function>'
} }
} }
export function toNull(): Value { export function toNull(): Value {
return toValue(null) return toValue(null)
}
const WRAPPED_MARKER = Symbol('reef-wrapped')
export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> {
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]
} }

View File

@ -3,7 +3,7 @@ import type { ExceptionHandler } from "./exception"
import { type Frame } from "./frame" import { type Frame } from "./frame"
import { OpCode } from "./opcode" import { OpCode } from "./opcode"
import { Scope } from "./scope" 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> | Value type NativeFunction = (...args: Value[]) => Promise<Value> | Value
@ -26,7 +26,14 @@ export class VM {
this.scope = new Scope() 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) this.nativeFunctions.set(name, fn)
} }

View File

@ -12,8 +12,8 @@ test("CALL_NATIVE - basic function call", async () => {
const vm = new VM(bytecode) const vm = new VM(bytecode)
// Register a native function // Register a Value-based function
vm.registerFunction('add', (a, b) => { vm.registerValueFunction('add', (a, b) => {
return toValue(toNumber(a) + toNumber(b)) return toValue(toNumber(a) + toNumber(b))
}) })
@ -30,7 +30,7 @@ test("CALL_NATIVE - function with string manipulation", async () => {
const vm = new VM(bytecode) 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 aStr = a.type === 'string' ? a.value : toString(a)
const bStr = b.type === 'string' ? b.value : toString(b) const bStr = b.type === 'string' ? b.value : toString(b)
return toValue(aStr + ' ' + bStr) return toValue(aStr + ' ' + bStr)
@ -48,7 +48,7 @@ test("CALL_NATIVE - async function", async () => {
const vm = new VM(bytecode) const vm = new VM(bytecode)
vm.registerFunction('asyncDouble', async (a) => { vm.registerValueFunction('asyncDouble', async (a) => {
// Simulate async operation // Simulate async operation
await new Promise(resolve => setTimeout(resolve, 1)) await new Promise(resolve => setTimeout(resolve, 1))
return toValue(toNumber(a) * 2) return toValue(toNumber(a) * 2)
@ -65,7 +65,7 @@ test("CALL_NATIVE - function with no arguments", async () => {
const vm = new VM(bytecode) const vm = new VM(bytecode)
vm.registerFunction('getAnswer', () => { vm.registerValueFunction('getAnswer', () => {
return toValue(42) return toValue(42)
}) })
@ -83,7 +83,7 @@ test("CALL_NATIVE - function with multiple arguments", async () => {
const vm = new VM(bytecode) const vm = new VM(bytecode)
vm.registerFunction('sum', (...args) => { vm.registerValueFunction('sum', (...args) => {
const total = args.reduce((acc, val) => acc + toNumber(val), 0) const total = args.reduce((acc, val) => acc + toNumber(val), 0)
return toValue(total) return toValue(total)
}) })
@ -100,7 +100,7 @@ test("CALL_NATIVE - function returns array", async () => {
const vm = new VM(bytecode) const vm = new VM(bytecode)
vm.registerFunction('makeRange', (n) => { vm.registerValueFunction('makeRange', (n) => {
const count = toNumber(n) const count = toNumber(n)
const arr = [] const arr = []
for (let i = 0; i < count; i++) { 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) const vm = new VM(bytecode)
vm.registerFunction('triple', (n) => { vm.registerValueFunction('triple', (n) => {
return toValue(toNumber(n) * 3) return toValue(toNumber(n) * 3)
}) })
const result = await vm.run() const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 25 }) 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 })
})