293 lines
6.7 KiB
TypeScript
293 lines
6.7 KiB
TypeScript
import { describe, test, expect } from 'bun:test'
|
|
import { Compiler } from '#compiler/compiler'
|
|
import { VM } from 'reefvm'
|
|
|
|
describe('Native Function Exceptions', () => {
|
|
test('native function error caught by try/catch', async () => {
|
|
const code = `
|
|
result = try:
|
|
failing-fn
|
|
catch e:
|
|
'caught: ' + e
|
|
end
|
|
|
|
result
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('failing-fn', () => {
|
|
throw new Error('native function failed')
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'caught: native function failed' })
|
|
})
|
|
|
|
test('async native function error caught by try/catch', async () => {
|
|
const code = `
|
|
result = try:
|
|
async-fail
|
|
catch e:
|
|
'async caught: ' + e
|
|
end
|
|
|
|
result
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('async-fail', async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 1))
|
|
throw new Error('async error')
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'async caught: async error' })
|
|
})
|
|
|
|
test('native function with arguments throwing error', async () => {
|
|
const code = `
|
|
result = try:
|
|
read-file missing.txt
|
|
catch e:
|
|
'default content'
|
|
end
|
|
|
|
result
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('read-file', (path: string) => {
|
|
if (path === 'missing.txt') {
|
|
throw new Error('file not found')
|
|
}
|
|
return 'file contents'
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'default content' })
|
|
})
|
|
|
|
test('native function error with finally block', async () => {
|
|
const code = `
|
|
cleanup-count = 0
|
|
|
|
result = try:
|
|
failing-fn
|
|
catch e:
|
|
'error handled'
|
|
finally:
|
|
cleanup-count = cleanup-count + 1
|
|
end
|
|
|
|
cleanup-count
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('failing-fn', () => {
|
|
throw new Error('native error')
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 1 })
|
|
})
|
|
|
|
test('native function error without catch propagates', async () => {
|
|
const code = `
|
|
failing-fn
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('failing-fn', () => {
|
|
throw new Error('uncaught error')
|
|
})
|
|
|
|
await expect(vm.run()).rejects.toThrow('uncaught error')
|
|
})
|
|
|
|
test('native function in function-level catch', async () => {
|
|
const code = `
|
|
safe-read = do path:
|
|
read-file path
|
|
catch e:
|
|
'default: ' + e
|
|
end
|
|
|
|
result = safe-read missing.txt
|
|
result
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('read-file', (path: string) => {
|
|
throw new Error('file not found: ' + path)
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'default: file not found: missing.txt' })
|
|
})
|
|
|
|
test('nested native function errors', async () => {
|
|
const code = `
|
|
result = try:
|
|
try:
|
|
inner-fail
|
|
catch e:
|
|
throw 'wrapped: ' + e
|
|
end
|
|
catch e:
|
|
'outer caught: ' + e
|
|
end
|
|
|
|
result
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('inner-fail', () => {
|
|
throw new Error('inner error')
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'outer caught: wrapped: inner error' })
|
|
})
|
|
|
|
test('native function error with multiple named args', async () => {
|
|
const code = `
|
|
result = try:
|
|
process-file path=missing.txt mode=strict
|
|
catch e:
|
|
'error: ' + e
|
|
end
|
|
|
|
result
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('process-file', (path: string, mode: string = 'lenient') => {
|
|
if (mode === 'strict' && path === 'missing.txt') {
|
|
throw new Error('strict mode: file required')
|
|
}
|
|
return 'processed'
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'error: strict mode: file required' })
|
|
})
|
|
|
|
test('native function returning normally after other functions threw', async () => {
|
|
const code = `
|
|
result1 = try:
|
|
failing-fn
|
|
catch e:
|
|
'caught'
|
|
end
|
|
|
|
result2 = success-fn
|
|
|
|
result1 + ' then ' + result2
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('failing-fn', () => {
|
|
throw new Error('error')
|
|
})
|
|
|
|
vm.set('success-fn', () => {
|
|
return 'success'
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'caught then success' })
|
|
})
|
|
|
|
test('native function error message preserved', async () => {
|
|
const code = `
|
|
result = try:
|
|
throw-custom-message
|
|
catch e:
|
|
e
|
|
end
|
|
|
|
result
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('throw-custom-message', () => {
|
|
throw new Error('This is a very specific error message with details')
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({
|
|
type: 'string',
|
|
value: 'This is a very specific error message with details'
|
|
})
|
|
})
|
|
|
|
test('native function throwing non-Error value', async () => {
|
|
const code = `
|
|
result = try:
|
|
throw-string
|
|
catch e:
|
|
'caught: ' + e
|
|
end
|
|
|
|
result
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('throw-string', () => {
|
|
throw 'plain string error'
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'caught: plain string error' })
|
|
})
|
|
|
|
test('multiple native function calls with mixed success/failure', async () => {
|
|
const code = `
|
|
r1 = try: success-fn catch e: 'error' end
|
|
r2 = try: failing-fn catch e: 'caught' end
|
|
r3 = try: success-fn catch e: 'error' end
|
|
|
|
results = [r1 r2 r3]
|
|
results
|
|
`
|
|
|
|
const compiler = new Compiler(code)
|
|
const vm = new VM(compiler.bytecode)
|
|
|
|
vm.set('success-fn', () => 'ok')
|
|
vm.set('failing-fn', () => {
|
|
throw new Error('failed')
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result.type).toBe('array')
|
|
const arr = result.value as any[]
|
|
expect(arr.length).toBe(3)
|
|
expect(arr[0]).toEqual({ type: 'string', value: 'ok' })
|
|
expect(arr[1]).toEqual({ type: 'string', value: 'caught' })
|
|
expect(arr[2]).toEqual({ type: 'string', value: 'ok' })
|
|
})
|
|
})
|