Compare commits
No commits in common. "797eb281cb39954e223d679ac72624386be0c936" and "eb4f103ba3693b3ff8137e1eb76f8c4caa81f320" have entirely different histories.
797eb281cb
...
eb4f103ba3
37
CLAUDE.md
37
CLAUDE.md
|
|
@ -169,43 +169,6 @@ Auto-wrapping handles:
|
||||||
- Sync and async functions
|
- Sync and async functions
|
||||||
- Arrays, objects, primitives, null, RegExp
|
- Arrays, objects, primitives, null, RegExp
|
||||||
|
|
||||||
### Calling Reef Functions from TypeScript
|
|
||||||
|
|
||||||
Use `vm.call()` to invoke Reef functions from TypeScript:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
MAKE_FUNCTION (x y=10) .add
|
|
||||||
STORE add
|
|
||||||
HALT
|
|
||||||
|
|
||||||
.add:
|
|
||||||
LOAD x
|
|
||||||
LOAD y
|
|
||||||
ADD
|
|
||||||
RETURN
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
await vm.run()
|
|
||||||
|
|
||||||
// Positional arguments
|
|
||||||
const result1 = await vm.call('add', 5, 3) // → 8
|
|
||||||
|
|
||||||
// Named arguments (pass final object)
|
|
||||||
const result2 = await vm.call('add', 5, { y: 20 }) // → 25
|
|
||||||
|
|
||||||
// All named arguments
|
|
||||||
const result3 = await vm.call('add', { x: 10, y: 15 }) // → 25
|
|
||||||
```
|
|
||||||
|
|
||||||
**How it works**:
|
|
||||||
- Looks up function in VM scope
|
|
||||||
- Converts it to a callable JavaScript function using `fnFromValue`
|
|
||||||
- Automatically converts arguments to ReefVM Values
|
|
||||||
- Executes the function in a fresh VM context
|
|
||||||
- Converts result back to JavaScript types
|
|
||||||
|
|
||||||
### Label Usage (Preferred)
|
### Label Usage (Preferred)
|
||||||
Use labels instead of numeric offsets for readability:
|
Use labels instead of numeric offsets for readability:
|
||||||
```
|
```
|
||||||
|
|
|
||||||
49
GUIDE.md
49
GUIDE.md
|
|
@ -676,55 +676,6 @@ CALL ; atOptions receives {debug: true, port: 8080}
|
||||||
|
|
||||||
Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object.
|
Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object.
|
||||||
|
|
||||||
### Calling Reef Functions from TypeScript
|
|
||||||
|
|
||||||
Once you have Reef functions defined in the VM, you can call them from TypeScript using `vm.call()`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
MAKE_FUNCTION (name greeting="Hello") .greet
|
|
||||||
STORE greet
|
|
||||||
HALT
|
|
||||||
|
|
||||||
.greet:
|
|
||||||
LOAD greeting
|
|
||||||
PUSH " "
|
|
||||||
LOAD name
|
|
||||||
PUSH "!"
|
|
||||||
STR_CONCAT #4
|
|
||||||
RETURN
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
await vm.run()
|
|
||||||
|
|
||||||
// Call with positional arguments
|
|
||||||
const result1 = await vm.call('greet', 'Alice')
|
|
||||||
// Returns: "Hello Alice!"
|
|
||||||
|
|
||||||
// Call with named arguments (pass as final object)
|
|
||||||
const result2 = await vm.call('greet', 'Bob', { greeting: 'Hi' })
|
|
||||||
// Returns: "Hi Bob!"
|
|
||||||
|
|
||||||
// Call with only named arguments
|
|
||||||
const result3 = await vm.call('greet', { name: 'Carol', greeting: 'Hey' })
|
|
||||||
// Returns: "Hey Carol!"
|
|
||||||
```
|
|
||||||
|
|
||||||
**How it works**:
|
|
||||||
- `vm.call(functionName, ...args)` looks up the function in the VM's scope
|
|
||||||
- Converts it to a callable JavaScript function
|
|
||||||
- Calls it with the provided arguments (automatically converted to ReefVM Values)
|
|
||||||
- Returns the result (automatically converted back to JavaScript types)
|
|
||||||
|
|
||||||
**Named arguments**: Pass a plain object as the final argument to provide named arguments. If the last argument is a non-array object, it's treated as named arguments. All preceding arguments are treated as positional.
|
|
||||||
|
|
||||||
**Type conversion**: Arguments and return values are automatically converted between JavaScript types and ReefVM Values:
|
|
||||||
- Primitives: `number`, `string`, `boolean`, `null`
|
|
||||||
- Arrays: converted recursively
|
|
||||||
- Objects: converted to ReefVM dicts
|
|
||||||
- Functions: Reef functions are converted to callable JavaScript functions
|
|
||||||
|
|
||||||
### Empty Stack
|
### Empty Stack
|
||||||
- RETURN with empty stack returns null
|
- RETURN with empty stack returns null
|
||||||
- HALT with empty stack returns null
|
- HALT with empty stack returns null
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,7 @@ Commands: `clear`, `reset`, `exit`.
|
||||||
- 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 with auto-wrapping for native TypeScript types
|
- Native function interop with auto-wrapping for native TypeScript types
|
||||||
- Native functions stored in scope, called via LOAD + CALL
|
- Native functions stored in scope, called via LOAD + CALL
|
||||||
- Native functions support `atXxx` parameters (e.g., `atOptions`) to collect unmatched named args
|
|
||||||
- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })`
|
- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })`
|
||||||
- Call Reef functions from TypeScript with `vm.call(name, ...args)` with automatic type conversion
|
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { type Value, type NativeFunction, fromValue, toValue } from "./value"
|
import { type Value, type NativeFunction, fromValue, toValue } from "./value"
|
||||||
import { VM } from "./vm"
|
|
||||||
|
|
||||||
export type ParamInfo = {
|
export type ParamInfo = {
|
||||||
params: string[]
|
params: string[]
|
||||||
|
|
@ -10,10 +9,10 @@ export type ParamInfo = {
|
||||||
|
|
||||||
const WRAPPED_MARKER = Symbol('reef-wrapped')
|
const WRAPPED_MARKER = Symbol('reef-wrapped')
|
||||||
|
|
||||||
export function wrapNative(vm: VM, fn: Function): (this: VM, ...args: Value[]) => Promise<Value> {
|
export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> {
|
||||||
const wrapped = async function (this: VM, ...values: Value[]) {
|
const wrapped = async (...values: Value[]) => {
|
||||||
const nativeArgs = values.map(arg => fromValue(arg, vm))
|
const nativeArgs = values.map(fromValue)
|
||||||
const result = await fn.call(this, ...nativeArgs)
|
const result = await fn(...nativeArgs)
|
||||||
return toValue(result)
|
return toValue(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
55
src/value.ts
55
src/value.ts
|
|
@ -1,6 +1,4 @@
|
||||||
import { OpCode } from "./opcode"
|
|
||||||
import { Scope } from "./scope"
|
import { Scope } from "./scope"
|
||||||
import { VM } from "./vm"
|
|
||||||
|
|
||||||
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
||||||
|
|
||||||
|
|
@ -156,22 +154,23 @@ export function isEqual(a: Value, b: Value): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromValue(v: Value, vm?: VM): any {
|
export function fromValue(v: Value): any {
|
||||||
switch (v.type) {
|
switch (v.type) {
|
||||||
case 'null':
|
case 'null':
|
||||||
|
return null
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
|
return v.value
|
||||||
case 'number':
|
case 'number':
|
||||||
|
return v.value
|
||||||
case 'string':
|
case 'string':
|
||||||
return v.value
|
return v.value
|
||||||
case 'array':
|
case 'array':
|
||||||
return v.value.map(x => fromValue(x, vm))
|
return v.value.map(fromValue)
|
||||||
case 'dict':
|
case 'dict':
|
||||||
return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v, vm)]))
|
return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)]))
|
||||||
case 'regex':
|
case 'regex':
|
||||||
return v.value
|
return v.value
|
||||||
case 'function':
|
case 'function':
|
||||||
if (!vm || !(vm instanceof VM)) throw new Error('VM is required for function conversion')
|
|
||||||
return fnFromValue(v, vm)
|
|
||||||
case 'native':
|
case 'native':
|
||||||
return '<function>'
|
return '<function>'
|
||||||
}
|
}
|
||||||
|
|
@ -180,45 +179,3 @@ export function fromValue(v: Value, vm?: VM): any {
|
||||||
export function toNull(): Value {
|
export function toNull(): Value {
|
||||||
return toValue(null)
|
return toValue(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fnFromValue(fn: Value, vm: VM): Function {
|
|
||||||
if (fn.type !== 'function')
|
|
||||||
throw new Error('Value is not a function')
|
|
||||||
|
|
||||||
return async function (...args: any[]) {
|
|
||||||
let positional: any[] = args
|
|
||||||
let named: Record<string, any> = {}
|
|
||||||
|
|
||||||
if (args.length > 0 && !Array.isArray(args[args.length - 1]) && args[args.length - 1].constructor === Object) {
|
|
||||||
named = args[args.length - 1]
|
|
||||||
positional = args.slice(0, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const newVM = new VM({
|
|
||||||
instructions: vm.instructions,
|
|
||||||
constants: vm.constants,
|
|
||||||
labels: vm.labels
|
|
||||||
})
|
|
||||||
newVM.scope = fn.parentScope
|
|
||||||
|
|
||||||
newVM.stack.push(fn)
|
|
||||||
newVM.stack.push(...positional.map(toValue))
|
|
||||||
for (const [key, val] of Object.entries(named)) {
|
|
||||||
newVM.stack.push(toValue(key))
|
|
||||||
newVM.stack.push(toValue(val))
|
|
||||||
}
|
|
||||||
newVM.stack.push(toValue(positional.length))
|
|
||||||
newVM.stack.push(toValue(Object.keys(named).length))
|
|
||||||
|
|
||||||
const targetDepth = newVM.callStack.length
|
|
||||||
await newVM.execute({ op: OpCode.CALL })
|
|
||||||
newVM.pc++
|
|
||||||
|
|
||||||
while (newVM.callStack.length > targetDepth && newVM.pc < newVM.instructions.length) {
|
|
||||||
await newVM.execute(newVM.instructions[newVM.pc]!)
|
|
||||||
newVM.pc++
|
|
||||||
}
|
|
||||||
|
|
||||||
return fromValue(newVM.stack.pop() || toNull(), vm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
src/vm.ts
24
src/vm.ts
|
|
@ -3,11 +3,9 @@ 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, type NativeFunction, toValue, toNumber, isTrue, isEqual, toString, fromValue, fnFromValue } from "./value"
|
import { type Value, type NativeFunction, toValue, toNumber, isTrue, isEqual, toString, fromValue } from "./value"
|
||||||
import { extractParamInfo, wrapNative, isWrapped, getOriginalFunction } from "./function"
|
import { extractParamInfo, wrapNative, isWrapped, getOriginalFunction } from "./function"
|
||||||
|
|
||||||
type Fn = (this: VM, ...args: any[]) => any
|
|
||||||
|
|
||||||
export class VM {
|
export class VM {
|
||||||
pc = 0
|
pc = 0
|
||||||
stopped = false
|
stopped = false
|
||||||
|
|
@ -20,7 +18,7 @@ export class VM {
|
||||||
labels: Map<number, string> = new Map()
|
labels: Map<number, string> = new Map()
|
||||||
nativeFunctions: Map<string, NativeFunction> = new Map()
|
nativeFunctions: Map<string, NativeFunction> = new Map()
|
||||||
|
|
||||||
constructor(bytecode: Bytecode, functions?: Record<string, Fn>) {
|
constructor(bytecode: Bytecode, functions?: Record<string, Function>) {
|
||||||
this.instructions = bytecode.instructions
|
this.instructions = bytecode.instructions
|
||||||
this.constants = bytecode.constants
|
this.constants = bytecode.constants
|
||||||
this.labels = bytecode.labels || new Map()
|
this.labels = bytecode.labels || new Map()
|
||||||
|
|
@ -31,18 +29,8 @@ export class VM {
|
||||||
this.registerFunction(name, functions[name]!)
|
this.registerFunction(name, functions[name]!)
|
||||||
}
|
}
|
||||||
|
|
||||||
async call(name: string, ...args: any) {
|
registerFunction(name: string, fn: Function) {
|
||||||
const value = this.scope.get(name)
|
const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(fn)
|
||||||
|
|
||||||
if (!value) throw new Error(`Can't find ${name}`)
|
|
||||||
if (value.type !== 'function') throw new Error(`Can't call ${name}`)
|
|
||||||
|
|
||||||
const fn = fnFromValue(value, this)
|
|
||||||
return await fn(...args)
|
|
||||||
}
|
|
||||||
|
|
||||||
registerFunction(name: string, fn: Fn) {
|
|
||||||
const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(this, fn)
|
|
||||||
this.scope.set(name, { type: 'native', fn: wrapped, value: '<function>' })
|
this.scope.set(name, { type: 'native', fn: wrapped, value: '<function>' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -488,7 +476,7 @@ export class VM {
|
||||||
namedDict.set(key, value)
|
namedDict.set(key, value)
|
||||||
}
|
}
|
||||||
// Convert dict to plain JavaScript object for the native function
|
// Convert dict to plain JavaScript object for the native function
|
||||||
const namedObj = fromValue({ type: 'dict', value: namedDict }, this)
|
const namedObj = fromValue({ type: 'dict', value: namedDict })
|
||||||
nativeArgs.push(toValue(namedObj))
|
nativeArgs.push(toValue(namedObj))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -501,7 +489,7 @@ export class VM {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the native function with bound args
|
// Call the native function with bound args
|
||||||
const result = await fn.fn.call(this, ...nativeArgs)
|
const result = await fn.fn(...nativeArgs)
|
||||||
this.stack.push(result)
|
this.stack.push(result)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -646,685 +646,3 @@ test("@named pattern - collects only unmatched named args", async () => {
|
||||||
expect(result.value.get('extra2')).toEqual(toValue('value2'))
|
expect(result.value.get('extra2')).toEqual(toValue('value2'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Native function receives Reef function as callback - basic map", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Define a Reef function that doubles a number
|
|
||||||
MAKE_FUNCTION (x) .double_body
|
|
||||||
STORE double
|
|
||||||
JUMP .skip_double
|
|
||||||
.double_body:
|
|
||||||
LOAD x
|
|
||||||
PUSH 2
|
|
||||||
MUL
|
|
||||||
RETURN
|
|
||||||
.skip_double:
|
|
||||||
|
|
||||||
; Call native 'map' with array and Reef function
|
|
||||||
LOAD map
|
|
||||||
PUSH 1
|
|
||||||
PUSH 2
|
|
||||||
PUSH 3
|
|
||||||
MAKE_ARRAY #3
|
|
||||||
LOAD double
|
|
||||||
PUSH 2
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
// Native function that takes an array and a callback
|
|
||||||
vm.registerFunction('map', async (array: any[], callback: Function) => {
|
|
||||||
const results = []
|
|
||||||
for (const item of array) {
|
|
||||||
results.push(await callback(item))
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result.type).toBe('array')
|
|
||||||
if (result.type === 'array') {
|
|
||||||
console.log(result.value)
|
|
||||||
expect(result.value).toEqual([
|
|
||||||
toValue(2),
|
|
||||||
toValue(4),
|
|
||||||
toValue(6)
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
expect(true).toBe(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function receives Reef function - iterator pattern with each", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Define a Reef function that adds to a sum
|
|
||||||
MAKE_FUNCTION (item) .add_to_sum_body
|
|
||||||
STORE add_to_sum
|
|
||||||
JUMP .skip_add_to_sum
|
|
||||||
.add_to_sum_body:
|
|
||||||
LOAD sum
|
|
||||||
LOAD item
|
|
||||||
ADD
|
|
||||||
STORE sum
|
|
||||||
RETURN
|
|
||||||
.skip_add_to_sum:
|
|
||||||
|
|
||||||
; Initialize sum
|
|
||||||
PUSH 0
|
|
||||||
STORE sum
|
|
||||||
|
|
||||||
; Call native 'each' with array and Reef function
|
|
||||||
LOAD each
|
|
||||||
PUSH 10
|
|
||||||
PUSH 20
|
|
||||||
PUSH 30
|
|
||||||
MAKE_ARRAY #3
|
|
||||||
LOAD add_to_sum
|
|
||||||
PUSH 2
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
|
|
||||||
; Return the sum
|
|
||||||
LOAD sum
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
// Native 'each' function (like Ruby's each)
|
|
||||||
vm.registerFunction('each', async (array: any[], callback: Function) => {
|
|
||||||
for (const item of array) {
|
|
||||||
await callback(item)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'number', value: 60 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function receives Reef function - filter pattern", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Define a Reef function that checks if number > 5
|
|
||||||
MAKE_FUNCTION (x) .is_greater_body
|
|
||||||
STORE is_greater
|
|
||||||
JUMP .skip_is_greater
|
|
||||||
.is_greater_body:
|
|
||||||
LOAD x
|
|
||||||
PUSH 5
|
|
||||||
GT
|
|
||||||
RETURN
|
|
||||||
.skip_is_greater:
|
|
||||||
|
|
||||||
; Call native 'filter' with array and Reef function
|
|
||||||
LOAD filter
|
|
||||||
PUSH 3
|
|
||||||
PUSH 7
|
|
||||||
PUSH 2
|
|
||||||
PUSH 9
|
|
||||||
PUSH 1
|
|
||||||
MAKE_ARRAY #5
|
|
||||||
LOAD is_greater
|
|
||||||
PUSH 2
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
// Native filter function
|
|
||||||
vm.registerFunction('filter', async (array: any[], predicate: Function) => {
|
|
||||||
const results = []
|
|
||||||
for (const item of array) {
|
|
||||||
if (await predicate(item)) {
|
|
||||||
results.push(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result.type).toBe('array')
|
|
||||||
if (result.type === 'array') {
|
|
||||||
expect(result.value).toEqual([
|
|
||||||
toValue(7),
|
|
||||||
toValue(9)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function receives Reef function - closure capturing", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Store a multiplier in scope
|
|
||||||
PUSH 10
|
|
||||||
STORE multiplier
|
|
||||||
|
|
||||||
; Define a Reef function that uses the closure variable
|
|
||||||
MAKE_FUNCTION (x) .multiply_body
|
|
||||||
STORE multiply_by_ten
|
|
||||||
JUMP .skip_multiply
|
|
||||||
.multiply_body:
|
|
||||||
LOAD x
|
|
||||||
LOAD multiplier
|
|
||||||
MUL
|
|
||||||
RETURN
|
|
||||||
.skip_multiply:
|
|
||||||
|
|
||||||
; Call native 'map' with the closure function
|
|
||||||
LOAD map
|
|
||||||
PUSH 1
|
|
||||||
PUSH 2
|
|
||||||
PUSH 3
|
|
||||||
MAKE_ARRAY #3
|
|
||||||
LOAD multiply_by_ten
|
|
||||||
PUSH 2
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('map', async (array: any[], callback: Function) => {
|
|
||||||
const results = []
|
|
||||||
for (const item of array) {
|
|
||||||
results.push(await callback(item))
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result.type).toBe('array')
|
|
||||||
if (result.type === 'array') {
|
|
||||||
expect(result.value).toEqual([
|
|
||||||
toValue(10),
|
|
||||||
toValue(20),
|
|
||||||
toValue(30)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function receives Reef function - multiple arguments", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Define a Reef function that takes two args
|
|
||||||
MAKE_FUNCTION (a b) .add_body
|
|
||||||
STORE add
|
|
||||||
JUMP .skip_add
|
|
||||||
.add_body:
|
|
||||||
LOAD a
|
|
||||||
LOAD b
|
|
||||||
ADD
|
|
||||||
RETURN
|
|
||||||
.skip_add:
|
|
||||||
|
|
||||||
; Call native 'reduce' with array, initial value, and Reef function
|
|
||||||
LOAD reduce
|
|
||||||
PUSH 1
|
|
||||||
PUSH 2
|
|
||||||
PUSH 3
|
|
||||||
PUSH 4
|
|
||||||
MAKE_ARRAY #4
|
|
||||||
PUSH 0
|
|
||||||
LOAD add
|
|
||||||
PUSH 3
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
// Native reduce function (accumulator, array, callback)
|
|
||||||
vm.registerFunction('reduce', async (array: any[], initial: any, callback: Function) => {
|
|
||||||
let acc = initial
|
|
||||||
for (const item of array) {
|
|
||||||
acc = await callback(acc, item)
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'number', value: 10 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function receives Reef function - returns non-primitive", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Define a Reef function that returns a dict
|
|
||||||
MAKE_FUNCTION (name) .make_user_body
|
|
||||||
STORE make_user
|
|
||||||
JUMP .skip_make_user
|
|
||||||
.make_user_body:
|
|
||||||
PUSH "name"
|
|
||||||
LOAD name
|
|
||||||
PUSH "active"
|
|
||||||
PUSH true
|
|
||||||
MAKE_DICT #2
|
|
||||||
RETURN
|
|
||||||
.skip_make_user:
|
|
||||||
|
|
||||||
; Call native 'map' to create users
|
|
||||||
LOAD map
|
|
||||||
PUSH "Alice"
|
|
||||||
PUSH "Bob"
|
|
||||||
MAKE_ARRAY #2
|
|
||||||
LOAD make_user
|
|
||||||
PUSH 2
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('map', async (array: any[], callback: Function) => {
|
|
||||||
const results = []
|
|
||||||
for (const item of array) {
|
|
||||||
results.push(await callback(item))
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result.type).toBe('array')
|
|
||||||
if (result.type === 'array') {
|
|
||||||
expect(result.value.length).toBe(2)
|
|
||||||
|
|
||||||
const first = result.value[0]
|
|
||||||
expect(first?.type).toBe('dict')
|
|
||||||
if (first?.type === 'dict') {
|
|
||||||
expect(first.value.get('name')).toEqual(toValue('Alice'))
|
|
||||||
expect(first.value.get('active')).toEqual(toValue(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
const second = result.value[1]
|
|
||||||
expect(second?.type).toBe('dict')
|
|
||||||
if (second?.type === 'dict') {
|
|
||||||
expect(second.value.get('name')).toEqual(toValue('Bob'))
|
|
||||||
expect(second.value.get('active')).toEqual(toValue(true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function calls Reef function - basic", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Define a Reef function that doubles a number
|
|
||||||
MAKE_FUNCTION (x) .double_body
|
|
||||||
STORE double
|
|
||||||
JUMP .skip_double
|
|
||||||
.double_body:
|
|
||||||
LOAD x
|
|
||||||
PUSH 2
|
|
||||||
MUL
|
|
||||||
RETURN
|
|
||||||
.skip_double:
|
|
||||||
|
|
||||||
; Call native function that will call the Reef function
|
|
||||||
LOAD process
|
|
||||||
PUSH 5
|
|
||||||
PUSH 1
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('process', async function (n: number) {
|
|
||||||
return await this.call('double', n)
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'number', value: 10 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function calls multiple Reef functions", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Define helper functions
|
|
||||||
MAKE_FUNCTION (x) .double_body
|
|
||||||
STORE double
|
|
||||||
JUMP .skip_double
|
|
||||||
.double_body:
|
|
||||||
LOAD x
|
|
||||||
PUSH 2
|
|
||||||
MUL
|
|
||||||
RETURN
|
|
||||||
.skip_double:
|
|
||||||
|
|
||||||
MAKE_FUNCTION (x) .triple_body
|
|
||||||
STORE triple
|
|
||||||
JUMP .skip_triple
|
|
||||||
.triple_body:
|
|
||||||
LOAD x
|
|
||||||
PUSH 3
|
|
||||||
MUL
|
|
||||||
RETURN
|
|
||||||
.skip_triple:
|
|
||||||
|
|
||||||
; Call native orchestrator
|
|
||||||
LOAD orchestrate
|
|
||||||
PUSH 5
|
|
||||||
PUSH 1
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('orchestrate', async function (n: number) {
|
|
||||||
const doubled = await this.call('double', n)
|
|
||||||
const tripled = await this.call('triple', n)
|
|
||||||
|
|
||||||
return doubled + tripled // 10 + 15 = 25
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'number', value: 25 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function conditionally calls Reef functions", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Define validation functions
|
|
||||||
MAKE_FUNCTION (x) .is_positive_body
|
|
||||||
STORE is_positive
|
|
||||||
JUMP .skip_is_positive
|
|
||||||
.is_positive_body:
|
|
||||||
LOAD x
|
|
||||||
PUSH 0
|
|
||||||
GT
|
|
||||||
RETURN
|
|
||||||
.skip_is_positive:
|
|
||||||
|
|
||||||
MAKE_FUNCTION (x) .negate_body
|
|
||||||
STORE negate
|
|
||||||
JUMP .skip_negate
|
|
||||||
.negate_body:
|
|
||||||
PUSH 0
|
|
||||||
LOAD x
|
|
||||||
SUB
|
|
||||||
RETURN
|
|
||||||
.skip_negate:
|
|
||||||
|
|
||||||
; Test with positive number
|
|
||||||
LOAD validate
|
|
||||||
PUSH 5
|
|
||||||
PUSH 1
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
STORE result1
|
|
||||||
|
|
||||||
; Test with negative number
|
|
||||||
LOAD validate
|
|
||||||
PUSH -3
|
|
||||||
PUSH 1
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
STORE result2
|
|
||||||
|
|
||||||
; Return sum
|
|
||||||
LOAD result1
|
|
||||||
LOAD result2
|
|
||||||
ADD
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('validate', async function (n: number) {
|
|
||||||
const isPositive = await this.call('is_positive', n)
|
|
||||||
|
|
||||||
if (isPositive) {
|
|
||||||
return n // Already positive
|
|
||||||
} else {
|
|
||||||
return await this.call('negate', n) // Make it positive
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'number', value: 8 }) // 5 + 3
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function calls Reef function with closure", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Set up a multiplier in scope
|
|
||||||
PUSH 10
|
|
||||||
STORE multiplier
|
|
||||||
|
|
||||||
; Define a Reef function that uses the closure variable
|
|
||||||
MAKE_FUNCTION (x) .multiply_body
|
|
||||||
STORE multiply_by_ten
|
|
||||||
JUMP .skip_multiply
|
|
||||||
.multiply_body:
|
|
||||||
LOAD x
|
|
||||||
LOAD multiplier
|
|
||||||
MUL
|
|
||||||
RETURN
|
|
||||||
.skip_multiply:
|
|
||||||
|
|
||||||
; Native function calls the closure
|
|
||||||
LOAD transform
|
|
||||||
PUSH 7
|
|
||||||
PUSH 1
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('transform', async function (n: number) {
|
|
||||||
return await this.call('multiply_by_ten', n)
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'number', value: 70 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function uses Reef function as filter predicate", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Define a predicate function
|
|
||||||
MAKE_FUNCTION (x) .is_even_body
|
|
||||||
STORE is_even
|
|
||||||
JUMP .skip_is_even
|
|
||||||
.is_even_body:
|
|
||||||
LOAD x
|
|
||||||
PUSH 2
|
|
||||||
MOD
|
|
||||||
PUSH 0
|
|
||||||
EQ
|
|
||||||
RETURN
|
|
||||||
.skip_is_even:
|
|
||||||
|
|
||||||
; Call native filter
|
|
||||||
LOAD filter_evens
|
|
||||||
PUSH 1
|
|
||||||
PUSH 2
|
|
||||||
PUSH 3
|
|
||||||
PUSH 4
|
|
||||||
PUSH 5
|
|
||||||
MAKE_ARRAY #5
|
|
||||||
PUSH 1
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('filter_evens', async function (array: any[]) {
|
|
||||||
const results = []
|
|
||||||
for (const item of array) {
|
|
||||||
if (await this.call('is_even', item)) {
|
|
||||||
results.push(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result.type).toBe('array')
|
|
||||||
if (result.type === 'array') {
|
|
||||||
expect(result.value).toEqual([
|
|
||||||
toValue(2),
|
|
||||||
toValue(4)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Reef calls native calls Reef - roundtrip", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Reef function that squares a number
|
|
||||||
MAKE_FUNCTION (x) .square_body
|
|
||||||
STORE square
|
|
||||||
JUMP .skip_square
|
|
||||||
.square_body:
|
|
||||||
LOAD x
|
|
||||||
LOAD x
|
|
||||||
MUL
|
|
||||||
RETURN
|
|
||||||
.skip_square:
|
|
||||||
|
|
||||||
; Reef function that calls native which calls back to Reef
|
|
||||||
MAKE_FUNCTION (x) .process_body
|
|
||||||
STORE process
|
|
||||||
JUMP .skip_process
|
|
||||||
.process_body:
|
|
||||||
LOAD native_helper
|
|
||||||
LOAD x
|
|
||||||
PUSH 1
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
RETURN
|
|
||||||
.skip_process:
|
|
||||||
|
|
||||||
; Call the Reef function
|
|
||||||
LOAD process
|
|
||||||
PUSH 3
|
|
||||||
PUSH 1
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('native_helper', async function (n: number) {
|
|
||||||
const squared = await this.call('square', n)
|
|
||||||
return squared + 1 // Add 1 to the squared result
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'number', value: 10 }) // 3^2 + 1 = 10
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function calls Reef function with multiple arguments", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Reef function that adds three numbers
|
|
||||||
MAKE_FUNCTION (a b c) .add_three_body
|
|
||||||
STORE add_three
|
|
||||||
JUMP .skip_add_three
|
|
||||||
.add_three_body:
|
|
||||||
LOAD a
|
|
||||||
LOAD b
|
|
||||||
ADD
|
|
||||||
LOAD c
|
|
||||||
ADD
|
|
||||||
RETURN
|
|
||||||
.skip_add_three:
|
|
||||||
|
|
||||||
; Native function that calls it
|
|
||||||
LOAD calculate
|
|
||||||
PUSH 0
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('calculate', async function () {
|
|
||||||
return await this.call('add_three', 10, 20, 30)
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'number', value: 60 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function calls Reef function that returns complex type", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Reef function that creates a user dict
|
|
||||||
MAKE_FUNCTION (name age) .make_user_body
|
|
||||||
STORE make_user
|
|
||||||
JUMP .skip_make_user
|
|
||||||
.make_user_body:
|
|
||||||
PUSH "name"
|
|
||||||
LOAD name
|
|
||||||
PUSH "age"
|
|
||||||
LOAD age
|
|
||||||
PUSH "active"
|
|
||||||
PUSH true
|
|
||||||
MAKE_DICT #3
|
|
||||||
RETURN
|
|
||||||
.skip_make_user:
|
|
||||||
|
|
||||||
; Native function calls it
|
|
||||||
LOAD create_user
|
|
||||||
PUSH 0
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('create_user', async function () {
|
|
||||||
return await this.call('make_user', "Alice", 30)
|
|
||||||
})
|
|
||||||
|
|
||||||
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))
|
|
||||||
expect(result.value.get('active')).toEqual(toValue(true))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function calls non-existent Reef function - throws error", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
LOAD bad_caller
|
|
||||||
PUSH 0
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('bad_caller', async function () {
|
|
||||||
return await this.call('nonexistent', 42)
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(vm.run()).rejects.toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Native function calls Reef function with named arguments", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
; Reef function with default parameters
|
|
||||||
MAKE_FUNCTION (name greeting='Hello') .greet_body
|
|
||||||
STORE greet
|
|
||||||
JUMP .skip_greet
|
|
||||||
.greet_body:
|
|
||||||
LOAD greeting
|
|
||||||
PUSH " "
|
|
||||||
LOAD name
|
|
||||||
PUSH "!"
|
|
||||||
STR_CONCAT #4
|
|
||||||
RETURN
|
|
||||||
.skip_greet:
|
|
||||||
|
|
||||||
LOAD call_greet
|
|
||||||
PUSH 0
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode, {
|
|
||||||
call_greet: async function () {
|
|
||||||
// Call with named argument as last positional (object)
|
|
||||||
return await this.call('greet', "Alice", { greeting: "Hi" })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'string', value: "Hi Alice!" })
|
|
||||||
})
|
|
||||||
Loading…
Reference in New Issue
Block a user