From 47e227f50c476e6865a2648397e131deea826462 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 7 Nov 2025 23:36:53 -0800 Subject: [PATCH] don't need vm for simple toValue calls --- src/value.ts | 39 ++++++++++++++----------- tests/value.test.ts | 69 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 78 insertions(+), 30 deletions(-) diff --git a/src/value.ts b/src/value.ts index e5133fc..a543caf 100644 --- a/src/value.ts +++ b/src/value.ts @@ -66,23 +66,9 @@ 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) { - const fnName = v.name || '' - const fnStr = v.toString().slice(0, 100) - const stack = new Error().stack || '' - const stackLines = stack.split('\n') - .slice(1) - .filter(line => !line.includes('toValue')) - .map(line => ' ' + line.trim()) - .join('\n') - throw new Error( - `can't toValue() function without a vm\n` + - ` Function: ${fnName}\n` + - ` Source: ${fnStr}${v.toString().length > 100 ? '...' : ''}\n` + - ` Called from:\n${stackLines}` - ) - } - return { type: 'native', fn: wrapNative(vm, v), value: '' } + + let fn = vm ? wrapNative(vm, v) : cantCallFunctionWithoutVM(v) + return { type: 'native', fn, value: '' } case 'object': const dict: Dict = new Map() @@ -94,6 +80,25 @@ export function toValue(v: any, vm?: VM): Value /* throws */ { } } +function cantCallFunctionWithoutVM(fn: Function) { + const name = fn.name || '' + const str = fn.toString().slice(0, 100) + return (...args: Value[]) => { + const stack = new Error().stack || '' + const stackLines = stack.split('\n') + .slice(1) + .filter(line => !line.includes('toValue')) + .map(line => ' ' + line.trim()) + .join('\n') + throw new Error( + `can't call function that was converted without a vm\n` + + ` Function: ${name}\n` + + ` Source: ${str}${str.length > 100 ? '...' : ''}\n` + + ` Called from:\n${stackLines}` + ) + } +} + export function toNumber(v: Value): number { switch (v.type) { case 'number': diff --git a/tests/value.test.ts b/tests/value.test.ts index 570c8ce..320ac0b 100644 --- a/tests/value.test.ts +++ b/tests/value.test.ts @@ -204,38 +204,81 @@ test("fromValue - async native function roundtrip", async () => { expect(result).toBe(12) }) -test("toValue - throws helpful error when converting function without VM", () => { +test("toValue - throws helpful error when calling function converted without VM", async () => { 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(/Called from:/) + const value = toValue(myFunction) + expect(value.type).toBe('native') + + // Error is thrown when calling the function, not when converting + await expect(async () => { + await value.fn({ type: 'number', value: 5 }) + }).toThrow(/can't call function that was converted without a vm/) + + await expect(async () => { + await value.fn({ type: 'number', value: 5 }) + }).toThrow(/Function: myFunction/) + + await expect(async () => { + await value.fn({ type: 'number', value: 5 }) + }).toThrow(/Source:/) + + await expect(async () => { + await value.fn({ type: 'number', value: 5 }) + }).toThrow(/Called from:/) }) -test("toValue - error message shows function name from binding", () => { +test("toValue - error message shows function info for arrow functions", async () => { const anonymousFn = (x: number) => x * 2 - expect(() => toValue(anonymousFn)).toThrow(/Function: anonymousFn/) - expect(() => toValue(anonymousFn)).toThrow(/Source:/) + const value = toValue(anonymousFn) + expect(value.type).toBe('native') + + // Arrow functions show as + await expect(async () => { + await value.fn({ type: 'number', value: 5 }) + }).toThrow(/Function: /) + + await expect(async () => { + await value.fn({ type: 'number', value: 5 }) + }).toThrow(/Source:/) }) -test("toValue - error when function is nested in object without VM", () => { +test("toValue - error when function is nested in object without VM", async () => { 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/) + const value = toValue(obj) + expect(value.type).toBe('dict') + + const handlerValue = value.value.get('handler')! + expect(handlerValue.type).toBe('native') + + await expect(async () => { + await (handlerValue as any).fn({ type: 'number', value: 5 }) + }).toThrow(/can't call function that was converted without a vm/) + + await expect(async () => { + await (handlerValue as any).fn({ type: 'number', value: 5 }) + }).toThrow(/Function: handler/) }) -test("toValue - error when function is nested in array without VM", () => { +test("toValue - error when function is nested in array without VM", async () => { const arr = [1, 2, (x: number) => x * 2] - expect(() => toValue(arr)).toThrow(/can't toValue\(\) function without a vm/) + const value = toValue(arr) + expect(value.type).toBe('array') + + const fnValue = value.value[2]! + expect(fnValue.type).toBe('native') + + await expect(async () => { + await (fnValue as any).fn({ type: 'number', value: 5 }) + }).toThrow(/can't call function that was converted without a vm/) }) test("fromValue - throws helpful error when converting function without VM", async () => {