diff --git a/bun.lock b/bun.lock index 005ca60..e8b8c92 100644 --- a/bun.lock +++ b/bun.lock @@ -62,7 +62,7 @@ "hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="], - "reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#030eb7487165b3ba502965a8b7fa09c4b5fdb0da", { "peerDependencies": { "typescript": "^5" } }, "030eb7487165b3ba502965a8b7fa09c4b5fdb0da"], + "reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#9618dd64148ccf6f0cdfd8a80a0f58efe3e0819d", { "peerDependencies": { "typescript": "^5" } }, "9618dd64148ccf6f0cdfd8a80a0f58efe3e0819d"], "style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="], diff --git a/src/compiler/tests/native-exceptions.test.ts b/src/compiler/tests/native-exceptions.test.ts new file mode 100644 index 0000000..a301fae --- /dev/null +++ b/src/compiler/tests/native-exceptions.test.ts @@ -0,0 +1,292 @@ +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 exception in native function: 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' }) + }) +}) diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 455a9b2..80cdef5 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -159,7 +159,7 @@ TryBlock { } Throw { - @specialize[@name=keyword] expression + @specialize[@name=keyword] (BinOp | ConditionalOp | expression) } ConditionalOp { diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index f1f7a33..ec84f41 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -7,9 +7,9 @@ import {highlighting} from "./highlight" const spec_Identifier = {__proto__:null,catch:82, finally:88, end:90, null:96, try:106, throw:110, if:114, elseif:122, else:126} export const parser = LRParser.deserialize({ version: 14, - states: "8lQYQbOOO#tQcO'#CvO$qOSO'#CxO%PQbO'#E`OOQ`'#DR'#DROOQa'#DO'#DOO&SQbO'#D]O'XQcO'#ETOOQa'#ET'#ETO)cQcO'#ESO*bQRO'#CwO+WQcO'#EOO+hQcO'#EOO+rQbO'#CuO,jOpO'#CsOOQ`'#EP'#EPO,oQbO'#EOO,vQQO'#EfOOQ`'#Db'#DbO,{QbO'#DdO,{QbO'#EhOOQ`'#Df'#DfO-pQRO'#DnOOQ`'#EO'#EOO-uQQO'#D}OOQ`'#D}'#D}OOQ`'#Do'#DoQYQbOOO-}QbO'#DPOOQa'#ES'#ESOOQ`'#D`'#D`OOQ`'#Ee'#EeOOQ`'#Dv'#DvO.XQbO,59^O.rQbO'#CzO.zQWO'#C{OOOO'#EV'#EVOOOO'#Dp'#DpO/`OSO,59dOOQa,59d,59dOOQ`'#Dr'#DrO/nQbO'#DSO/vQQO,5:zOOQ`'#Dq'#DqO/{QbO,59wO0SQQO,59jOOQa,59w,59wO0_QbO,59wO,{QbO,59cO,{QbO,59cO,{QbO,59cO,{QbO,59yO,{QbO,59yO,{QbO,59yO0iQRO,59aO0pQRO,59aO1RQRO,59aO0|QQO,59aO1^QQO,59aO1fObO,59_O1qQbO'#DwO1|QbO,59]O2eQbO,5;QOOQ`,5:O,5:OO2xQRO,5;SO3PQRO,5;SO3[QbO,5:YOOQ`,5:i,5:iOOQ`-E7m-E7mOOQ`,59k,59kOOQ`-E7t-E7tOOOO,59f,59fOOOO,59g,59gOOOO-E7n-E7nOOQa1G/O1G/OOOQ`-E7p-E7pO3lQbO1G0fOOQ`-E7o-E7oO4PQQO1G/UOOQa1G/c1G/cO4[QbO1G/cOOQO'#Dt'#DtO4PQQO1G/UOOQa1G/U1G/UOOQ`'#Du'#DuO4[QbO1G/cOOQa1G.}1G.}O5TQcO1G.}O5_QcO1G.}O5iQcO1G.}OOQa1G/e1G/eO7XQcO1G/eO7`QcO1G/eO7gQcO1G/eOOQa1G.{1G.{OOQa1G.y1G.yO!aQbO'#CvO&ZQbO'#CrOOQ`,5:c,5:cOOQ`-E7u-E7uO7nQbO1G0lO7yQbO1G0mO8gQbO1G0nOOQ`1G/t1G/tO8zQbO7+&QO7yQbO7+&SO9VQQO7+$pOOQa7+$p7+$pO9bQbO7+$}OOQa7+$}7+$}OOQO-E7r-E7rOOQ`-E7s-E7sO9lQbO'#DUO9qQQO'#DXOOQ`7+&W7+&WO9vQbO7+&WO9{QbO7+&WOOQ`'#Ds'#DsO:TQQO'#DsO:YQbO'#EaOOQ`'#DW'#DWO:|QbO7+&XOOQ`'#Dh'#DhO;XQbO7+&YO;^QbO7+&ZOOQ`<bQbO1G/_OOQ`1G/_1G/_OOQ`AN?^AN?^OOQ`AN?_AN?_O>xQbOAN?_O,{QbO'#DjOOQ`'#Dx'#DxO>}QbOAN?aO?YQQO'#DlOOQ`AN?aAN?aO?_QbOAN?aOOQ`G24rG24rOOQ`G24tG24tO?dQbOG24tO?iQbO7+$vOOQ`7+$v7+$vOOQ`7+$y7+$yOOQ`G24yG24yO@SQRO,5:UO@ZQRO,5:UOOQ`-E7v-E7vOOQ`G24{G24{O@fQbOG24{O@kQQO,5:WOOQ`LD*`LD*`OOQ`<bQbO1G/rO;^QbO7+%[OOQ`7+%^7+%^OOQ`<PQbO<[QQO,59pO>aQbO,59sOOQ`<tQbO<yQbO< (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 15, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], - tokenPrec: 1463 + tokenPrec: 1547 })