From 97b6722a113417398a1c47d583bfe07a906f87a0 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 26 Oct 2025 12:52:57 -0700 Subject: [PATCH] support roundtrip value conversions --- src/index.ts | 2 +- src/value.ts | 10 +++++++++- tests/value.test.ts | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1fab29d..ce72660 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,5 +13,5 @@ export { wrapNative, isWrapped, type ParamInfo, extractParamInfo, getOriginalFun export { OpCode } from "./opcode" export { Scope } from "./scope" export type { Value, TypeScriptFunction, NativeFunction } from "./value" -export { isValue, toValue, toString, toNumber, fromValue, toNull } from "./value" +export { isValue, toValue, toString, toNumber, fromValue, toNull, fnFromValue } from "./value" export { VM } from "./vm" \ No newline at end of file diff --git a/src/value.ts b/src/value.ts index be429e3..175c4a4 100644 --- a/src/value.ts +++ b/src/value.ts @@ -4,6 +4,7 @@ import { VM } from "./vm" export type NativeFunction = (...args: Value[]) => Promise | Value export type TypeScriptFunction = (this: VM, ...args: any[]) => any +const REEF_FUNCTION = Symbol('__reefFunction') export type Value = | { type: 'null', value: null } @@ -61,6 +62,8 @@ export function toValue(v: any): Value /* throws */ { case 'string': return { type: 'string', value: v } case 'function': + if ((v as any)[REEF_FUNCTION]) + return (v as any)[REEF_FUNCTION] throw new Error("can't toValue() a js function yet") case 'object': const dict: Dict = new Map() @@ -190,7 +193,7 @@ 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[]) { + const wrapper = async function (...args: any[]) { let positional: any[] = args let named: Record = {} @@ -226,4 +229,9 @@ export function fnFromValue(fn: Value, vm: VM): Function { return fromValue(newVM.stack.pop() || toNull(), vm) } + + // support roundtrips, eg fromValue(toValue(fn)) + ; (wrapper as any)[REEF_FUNCTION] = fn + + return wrapper } \ No newline at end of file diff --git a/tests/value.test.ts b/tests/value.test.ts index 193e087..46c1838 100644 --- a/tests/value.test.ts +++ b/tests/value.test.ts @@ -80,3 +80,36 @@ test("isValue - edge cases with type and value properties", () => { expect(isValue({ type: 'number', val: 42 })).toBe(false) expect(isValue({ typ: 'number', value: 42 })).toBe(false) }) + +test("toValue - converts wrapped Reef functions back to original Value", async () => { + const { VM, toBytecode, fnFromValue } = await import("#reef") + + // Create a Reef function + const bytecode = toBytecode([ + ["MAKE_FUNCTION", ["x"], ".body"], + ["STORE", "add1"], + ["JUMP", ".end"], + [".body:"], + ["LOAD", "x"], + ["PUSH", 1], + ["ADD"], + ["RETURN"], + [".end:"], + ["HALT"] + ]) + + const vm = new VM(bytecode) + await vm.run() + + const reefFunction = vm.scope.get("add1")! + expect(reefFunction.type).toBe("function") + + // Convert to JS function + const jsFunction = fnFromValue(reefFunction, vm) + expect(typeof jsFunction).toBe("function") + + // Convert back to Value - should return the original Reef function + const backToValue = toValue(jsFunction) + expect(backToValue).toBe(reefFunction) // Same reference + expect(backToValue.type).toBe("function") +})