shrimp/src/compiler/tests/native-exceptions.test.ts
2025-10-29 15:37:39 -07:00

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' })
})
})