diff --git a/src/value.ts b/src/value.ts index e52115a..d1f1d3e 100644 --- a/src/value.ts +++ b/src/value.ts @@ -65,7 +65,15 @@ export function toValue(v: any, vm?: VM): Value /* throws */ { case 'function': if ((v as any)[REEF_FUNCTION]) return (v as any)[REEF_FUNCTION] - if (!vm) throw new Error("can't toValue() function without a vm") + if (!vm) { + const fnName = v.name || '' + const fnStr = v.toString().slice(0, 100) + throw new Error( + `can't toValue() function without a vm\n` + + ` Function: ${fnName}\n` + + ` Source: ${fnStr}${v.toString().length > 100 ? '...' : ''}\n` + ) + } return { type: 'native', fn: wrapNative(vm, v), value: '' } case 'object': const dict: Dict = new Map() @@ -180,7 +188,14 @@ export function fromValue(v: Value, vm?: VM): any { case 'regex': return v.value case 'function': - if (!vm || !(vm instanceof VM)) throw new Error('VM is required for function conversion') + if (!vm || !(vm instanceof VM)) { + throw new Error( + `VM is required for function conversion\n` + + ` Function params: [${v.params.join(', ')}]\n` + + ` Function body at instruction: ${v.body}\n` + + ` Tip: Pass a VM instance as the second argument to fromValue()` + ) + } return fnFromValue(v, vm) case 'native': return getOriginalFunction(v.fn) diff --git a/tests/value.test.ts b/tests/value.test.ts index 22f42f5..c662de1 100644 --- a/tests/value.test.ts +++ b/tests/value.test.ts @@ -168,19 +168,70 @@ test("fromValue - async native function roundtrip", async () => { const bytecode = toBytecode([["HALT"]]) const vm = new VM(bytecode) - // Create an async native function const asyncFn = async (x: number, y: number) => { await new Promise(resolve => setTimeout(resolve, 1)) return x + y } - // Roundtrip through Value const nativeValue = toValue(asyncFn, vm) expect(nativeValue.type).toBe("native") const roundtrippedFn = fromValue(nativeValue, vm) - // Verify it works const result = await roundtrippedFn(5, 7) expect(result).toBe(12) }) + +test("toValue - throws helpful error when converting function without VM", () => { + function myFunction(x: number) { + return x * 2 + } + + expect(() => toValue(myFunction)).toThrow(/can't toValue\(\) function without a vm/) + expect(() => toValue(myFunction)).toThrow(/Function: myFunction/) + expect(() => toValue(myFunction)).toThrow(/Source:/) + expect(() => toValue(myFunction)).toThrow(/Tip: Pass a VM instance/) +}) + +test("toValue - error message shows function name from binding", () => { + const anonymousFn = (x: number) => x * 2 + + expect(() => toValue(anonymousFn)).toThrow(/Function: anonymousFn/) + expect(() => toValue(anonymousFn)).toThrow(/Source:/) +}) + +test("toValue - error when function is nested in object without VM", () => { + const obj = { + name: "test", + handler: (x: number) => x * 2 + } + + expect(() => toValue(obj)).toThrow(/can't toValue\(\) function without a vm/) + expect(() => toValue(obj)).toThrow(/Function: handler/) +}) + +test("toValue - error when function is nested in array without VM", () => { + const arr = [1, 2, (x: number) => x * 2] + + expect(() => toValue(arr)).toThrow(/can't toValue\(\) function without a vm/) +}) + +test("fromValue - throws helpful error when converting function without VM", async () => { + const { Scope, fromValue } = await import("#reef") + + const reefFunction = { + type: 'function' as const, + params: ['x', 'y'], + defaults: {}, + body: 10, + parentScope: new Scope(), + variadic: false, + named: false, + value: '' as const + } + + expect(() => fromValue(reefFunction)).toThrow(/VM is required for function conversion/) + expect(() => fromValue(reefFunction)).toThrow(/Function params: \[x, y\]/) + expect(() => fromValue(reefFunction)).toThrow(/Function body at instruction: 10/) + expect(() => fromValue(reefFunction)).toThrow(/Tip: Pass a VM instance/) +})