don't need vm for simple toValue calls

This commit is contained in:
Chris Wanstrath 2025-11-07 23:36:53 -08:00
parent 15884ac239
commit 47e227f50c
2 changed files with 78 additions and 30 deletions

View File

@ -66,23 +66,9 @@ export function toValue(v: any, vm?: VM): Value /* throws */ {
case 'function': case 'function':
if ((v as any)[REEF_FUNCTION]) if ((v as any)[REEF_FUNCTION])
return (v as any)[REEF_FUNCTION] return (v as any)[REEF_FUNCTION]
if (!vm) {
const fnName = v.name || '<anonymous>' let fn = vm ? wrapNative(vm, v) : cantCallFunctionWithoutVM(v)
const fnStr = v.toString().slice(0, 100) return { type: 'native', fn, value: '<function>' }
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: '<function>' }
case 'object': case 'object':
const dict: Dict = new Map() 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 || '<anonymous>'
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 { export function toNumber(v: Value): number {
switch (v.type) { switch (v.type) {
case 'number': case 'number':

View File

@ -204,38 +204,81 @@ test("fromValue - async native function roundtrip", async () => {
expect(result).toBe(12) 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) { function myFunction(x: number) {
return x * 2 return x * 2
} }
expect(() => toValue(myFunction)).toThrow(/can't toValue\(\) function without a vm/) const value = toValue(myFunction)
expect(() => toValue(myFunction)).toThrow(/Function: myFunction/) expect(value.type).toBe('native')
expect(() => toValue(myFunction)).toThrow(/Source:/)
expect(() => toValue(myFunction)).toThrow(/Called from:/) // 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 const anonymousFn = (x: number) => x * 2
expect(() => toValue(anonymousFn)).toThrow(/Function: anonymousFn/) const value = toValue(anonymousFn)
expect(() => toValue(anonymousFn)).toThrow(/Source:/) expect(value.type).toBe('native')
// Arrow functions show as <anonymous>
await expect(async () => {
await value.fn({ type: 'number', value: 5 })
}).toThrow(/Function: <anonymous>/)
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 = { const obj = {
name: "test", name: "test",
handler: (x: number) => x * 2 handler: (x: number) => x * 2
} }
expect(() => toValue(obj)).toThrow(/can't toValue\(\) function without a vm/) const value = toValue(obj)
expect(() => toValue(obj)).toThrow(/Function: handler/) 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] 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 () => { test("fromValue - throws helpful error when converting function without VM", async () => {