give more info on toValue() error

This commit is contained in:
Chris Wanstrath 2025-11-05 15:11:21 -08:00
parent 33ea94a247
commit 11b119a322
2 changed files with 71 additions and 5 deletions

View File

@ -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 || '<anonymous>'
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: '<function>' }
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)

View File

@ -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: '<function>' 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/)
})