more natural native functions
This commit is contained in:
parent
146b0a2883
commit
78923b3eff
41
CLAUDE.md
41
CLAUDE.md
|
|
@ -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:
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
30
src/value.ts
30
src/value.ts
|
|
@ -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]
|
||||||
}
|
}
|
||||||
11
src/vm.ts
11
src/vm.ts
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user