Compare commits
No commits in common. "0d73789a2547d8fff49ed7312a2bc874eaae1ab0" and "bc0684185a1179c7d48dcc360fbc486a99358266" have entirely different histories.
0d73789a25
...
bc0684185a
2
bun.lock
2
bun.lock
|
|
@ -62,7 +62,7 @@
|
||||||
|
|
||||||
"hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="],
|
"hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="],
|
||||||
|
|
||||||
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#c69b172c78853756ec8acba5bc33d93eb6a571c6", { "peerDependencies": { "typescript": "^5" } }, "c69b172c78853756ec8acba5bc33d93eb6a571c6"],
|
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#030eb7487165b3ba502965a8b7fa09c4b5fdb0da", { "peerDependencies": { "typescript": "^5" } }, "030eb7487165b3ba502965a8b7fa09c4b5fdb0da"],
|
||||||
|
|
||||||
"style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="],
|
"style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"dev": "bun generate-parser && bun --hot src/server/server.tsx",
|
"dev": "bun generate-parser && bun --hot src/server/server.tsx",
|
||||||
"generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts",
|
"generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts",
|
||||||
"repl": "bun generate-parser && bun bin/repl",
|
"repl": "bun generate-parser && bun bin/repl",
|
||||||
"update-reef": "rm -rf ~/.bun/install/cache/ && bun update reefvm"
|
"update-reef": "cd packages/ReefVM && git pull origin main"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/view": "^6.38.3",
|
"@codemirror/view": "^6.38.3",
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import {
|
||||||
getNamedArgParts,
|
getNamedArgParts,
|
||||||
getPipeExprParts,
|
getPipeExprParts,
|
||||||
getStringParts,
|
getStringParts,
|
||||||
getTryExprParts,
|
|
||||||
} from '#compiler/utils'
|
} from '#compiler/utils'
|
||||||
|
|
||||||
const DEBUG = false
|
const DEBUG = false
|
||||||
|
|
@ -53,7 +52,6 @@ export class Compiler {
|
||||||
instructions: ProgramItem[] = []
|
instructions: ProgramItem[] = []
|
||||||
fnLabelCount = 0
|
fnLabelCount = 0
|
||||||
ifLabelCount = 0
|
ifLabelCount = 0
|
||||||
tryLabelCount = 0
|
|
||||||
bytecode: Bytecode
|
bytecode: Bytecode
|
||||||
pipeCounter = 0
|
pipeCounter = 0
|
||||||
|
|
||||||
|
|
@ -284,10 +282,7 @@ export class Compiler {
|
||||||
}
|
}
|
||||||
|
|
||||||
case terms.FunctionDef: {
|
case terms.FunctionDef: {
|
||||||
const { paramNames, bodyNodes, catchVariable, catchBody, finallyBody } = getFunctionDefParts(
|
const { paramNames, bodyNodes } = getFunctionDefParts(node, input)
|
||||||
node,
|
|
||||||
input
|
|
||||||
)
|
|
||||||
const instructions: ProgramItem[] = []
|
const instructions: ProgramItem[] = []
|
||||||
const functionLabel: Label = `.func_${this.fnLabelCount++}`
|
const functionLabel: Label = `.func_${this.fnLabelCount++}`
|
||||||
const afterLabel: Label = `.after_${functionLabel}`
|
const afterLabel: Label = `.after_${functionLabel}`
|
||||||
|
|
@ -295,27 +290,9 @@ export class Compiler {
|
||||||
instructions.push(['JUMP', afterLabel])
|
instructions.push(['JUMP', afterLabel])
|
||||||
|
|
||||||
instructions.push([`${functionLabel}:`])
|
instructions.push([`${functionLabel}:`])
|
||||||
|
bodyNodes.forEach((bodyNode) => {
|
||||||
const compileFunctionBody = () => {
|
instructions.push(...this.#compileNode(bodyNode, input))
|
||||||
const bodyInstructions: ProgramItem[] = []
|
})
|
||||||
bodyNodes.forEach((bodyNode, index) => {
|
|
||||||
bodyInstructions.push(...this.#compileNode(bodyNode, input))
|
|
||||||
if (index < bodyNodes.length - 1) {
|
|
||||||
bodyInstructions.push(['POP'])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return bodyInstructions
|
|
||||||
}
|
|
||||||
|
|
||||||
if (catchVariable || finallyBody) {
|
|
||||||
// If function has catch or finally, wrap body in try/catch/finally
|
|
||||||
instructions.push(
|
|
||||||
...this.#compileTryCatchFinally(compileFunctionBody, catchVariable, catchBody, finallyBody, input)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
instructions.push(...compileFunctionBody())
|
|
||||||
}
|
|
||||||
|
|
||||||
instructions.push(['RETURN'])
|
instructions.push(['RETURN'])
|
||||||
|
|
||||||
instructions.push([`${afterLabel}:`])
|
instructions.push([`${afterLabel}:`])
|
||||||
|
|
@ -369,48 +346,10 @@ export class Compiler {
|
||||||
}
|
}
|
||||||
|
|
||||||
case terms.ThenBlock:
|
case terms.ThenBlock:
|
||||||
case terms.SingleLineThenBlock:
|
case terms.SingleLineThenBlock: {
|
||||||
case terms.TryBlock: {
|
const instructions = getAllChildren(node)
|
||||||
const children = getAllChildren(node)
|
.map((child) => this.#compileNode(child, input))
|
||||||
const instructions: ProgramItem[] = []
|
.flat()
|
||||||
|
|
||||||
children.forEach((child, index) => {
|
|
||||||
instructions.push(...this.#compileNode(child, input))
|
|
||||||
// keep only the last expression's value
|
|
||||||
if (index < children.length - 1) {
|
|
||||||
instructions.push(['POP'])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return instructions
|
|
||||||
}
|
|
||||||
|
|
||||||
case terms.TryExpr: {
|
|
||||||
const { tryBlock, catchVariable, catchBody, finallyBody } = getTryExprParts(node, input)
|
|
||||||
|
|
||||||
return this.#compileTryCatchFinally(
|
|
||||||
() => this.#compileNode(tryBlock, input),
|
|
||||||
catchVariable,
|
|
||||||
catchBody,
|
|
||||||
finallyBody,
|
|
||||||
input
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
case terms.Throw: {
|
|
||||||
const children = getAllChildren(node)
|
|
||||||
const [_throwKeyword, expression] = children
|
|
||||||
if (!expression) {
|
|
||||||
throw new CompilerError(
|
|
||||||
`Throw expected expression, got ${children.length} children`,
|
|
||||||
node.from,
|
|
||||||
node.to
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const instructions: ProgramItem[] = []
|
|
||||||
instructions.push(...this.#compileNode(expression, input))
|
|
||||||
instructions.push(['THROW'])
|
|
||||||
|
|
||||||
return instructions
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
@ -617,52 +556,4 @@ export class Compiler {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#compileTryCatchFinally(
|
|
||||||
compileTryBody: () => ProgramItem[],
|
|
||||||
catchVariable: string | undefined,
|
|
||||||
catchBody: SyntaxNode | undefined,
|
|
||||||
finallyBody: SyntaxNode | undefined,
|
|
||||||
input: string
|
|
||||||
): ProgramItem[] {
|
|
||||||
const instructions: ProgramItem[] = []
|
|
||||||
this.tryLabelCount++
|
|
||||||
const catchLabel: Label = `.catch_${this.tryLabelCount}`
|
|
||||||
const finallyLabel: Label = finallyBody ? `.finally_${this.tryLabelCount}` : (null as any)
|
|
||||||
const endLabel: Label = `.end_try_${this.tryLabelCount}`
|
|
||||||
|
|
||||||
instructions.push(['PUSH_TRY', catchLabel])
|
|
||||||
instructions.push(...compileTryBody())
|
|
||||||
instructions.push(['POP_TRY'])
|
|
||||||
instructions.push(['JUMP', finallyBody ? finallyLabel : endLabel])
|
|
||||||
|
|
||||||
// catch block
|
|
||||||
instructions.push([`${catchLabel}:`])
|
|
||||||
if (catchBody && catchVariable) {
|
|
||||||
instructions.push(['STORE', catchVariable])
|
|
||||||
const catchInstructions = this.#compileNode(catchBody, input)
|
|
||||||
instructions.push(...catchInstructions)
|
|
||||||
instructions.push(['JUMP', finallyBody ? finallyLabel : endLabel])
|
|
||||||
} else {
|
|
||||||
// no catch block
|
|
||||||
if (finallyBody) {
|
|
||||||
instructions.push(['JUMP', finallyLabel])
|
|
||||||
} else {
|
|
||||||
instructions.push(['THROW'])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// finally block
|
|
||||||
if (finallyBody) {
|
|
||||||
instructions.push([`${finallyLabel}:`])
|
|
||||||
const finallyInstructions = this.#compileNode(finallyBody, input)
|
|
||||||
instructions.push(...finallyInstructions)
|
|
||||||
// finally doesn't return a value
|
|
||||||
instructions.push(['POP'])
|
|
||||||
}
|
|
||||||
|
|
||||||
instructions.push([`${endLabel}:`])
|
|
||||||
|
|
||||||
return instructions
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,311 +0,0 @@
|
||||||
import { describe } from 'bun:test'
|
|
||||||
import { expect, test } from 'bun:test'
|
|
||||||
|
|
||||||
describe('exception handling', () => {
|
|
||||||
test('try with catch - no error thrown', () => {
|
|
||||||
expect(`
|
|
||||||
try:
|
|
||||||
42
|
|
||||||
catch err:
|
|
||||||
99
|
|
||||||
end
|
|
||||||
`).toEvaluateTo(42)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('try with catch - error thrown', () => {
|
|
||||||
expect(`
|
|
||||||
try:
|
|
||||||
throw 'something went wrong'
|
|
||||||
99
|
|
||||||
catch err:
|
|
||||||
err
|
|
||||||
end
|
|
||||||
`).toEvaluateTo('something went wrong')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('try with catch - catch variable binding', () => {
|
|
||||||
expect(`
|
|
||||||
try:
|
|
||||||
throw 100
|
|
||||||
catch my-error:
|
|
||||||
my-error + 50
|
|
||||||
end
|
|
||||||
`).toEvaluateTo(150)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('try with finally - no error', () => {
|
|
||||||
expect(`
|
|
||||||
x = 0
|
|
||||||
result = try:
|
|
||||||
x = 10
|
|
||||||
42
|
|
||||||
finally:
|
|
||||||
x = x + 5
|
|
||||||
end
|
|
||||||
x
|
|
||||||
`).toEvaluateTo(15)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('try with finally - return value from try', () => {
|
|
||||||
expect(`
|
|
||||||
x = 0
|
|
||||||
result = try:
|
|
||||||
x = 10
|
|
||||||
42
|
|
||||||
finally:
|
|
||||||
x = x + 5
|
|
||||||
999
|
|
||||||
end
|
|
||||||
result
|
|
||||||
`).toEvaluateTo(42)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('try with catch and finally - no error', () => {
|
|
||||||
expect(`
|
|
||||||
x = 0
|
|
||||||
try:
|
|
||||||
x = 10
|
|
||||||
42
|
|
||||||
catch err:
|
|
||||||
x = 999
|
|
||||||
0
|
|
||||||
finally:
|
|
||||||
x = x + 5
|
|
||||||
end
|
|
||||||
x
|
|
||||||
`).toEvaluateTo(15)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('try with catch and finally - error thrown', () => {
|
|
||||||
expect(`
|
|
||||||
x = 0
|
|
||||||
result = try:
|
|
||||||
x = 10
|
|
||||||
throw 'error'
|
|
||||||
99
|
|
||||||
catch err:
|
|
||||||
x = 20
|
|
||||||
err
|
|
||||||
finally:
|
|
||||||
x = x + 5
|
|
||||||
end
|
|
||||||
x
|
|
||||||
`).toEvaluateTo(25)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('try with catch and finally - return value from catch', () => {
|
|
||||||
expect(`
|
|
||||||
result = try:
|
|
||||||
throw 'oops'
|
|
||||||
catch err:
|
|
||||||
'caught'
|
|
||||||
finally:
|
|
||||||
'finally'
|
|
||||||
end
|
|
||||||
result
|
|
||||||
`).toEvaluateTo('caught')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('throw statement with string', () => {
|
|
||||||
expect(`
|
|
||||||
try:
|
|
||||||
throw 'error message'
|
|
||||||
catch err:
|
|
||||||
err
|
|
||||||
end
|
|
||||||
`).toEvaluateTo('error message')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('throw statement with number', () => {
|
|
||||||
expect(`
|
|
||||||
try:
|
|
||||||
throw 404
|
|
||||||
catch err:
|
|
||||||
err
|
|
||||||
end
|
|
||||||
`).toEvaluateTo(404)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('throw statement with dict', () => {
|
|
||||||
expect(`
|
|
||||||
try:
|
|
||||||
throw [code=500 message=failed]
|
|
||||||
catch e:
|
|
||||||
e
|
|
||||||
end
|
|
||||||
`).toEvaluateTo({ code: 500, message: 'failed' })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('uncaught exception fails', () => {
|
|
||||||
expect(`throw 'uncaught error'`).toFailEvaluation()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('single-line try catch', () => {
|
|
||||||
expect(`result = try: throw 'err' catch e: 'handled' end; result`).toEvaluateTo('handled')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('nested try blocks - inner catches', () => {
|
|
||||||
expect(`
|
|
||||||
try:
|
|
||||||
result = try:
|
|
||||||
throw 'inner error'
|
|
||||||
catch err:
|
|
||||||
err
|
|
||||||
end
|
|
||||||
result
|
|
||||||
catch outer:
|
|
||||||
'outer'
|
|
||||||
end
|
|
||||||
`).toEvaluateTo('inner error')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('nested try blocks - outer catches', () => {
|
|
||||||
expect(`
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
throw 'inner error'
|
|
||||||
catch err:
|
|
||||||
throw 'outer error'
|
|
||||||
end
|
|
||||||
catch outer:
|
|
||||||
outer
|
|
||||||
end
|
|
||||||
`).toEvaluateTo('outer error')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('try as expression', () => {
|
|
||||||
expect(`
|
|
||||||
x = try: 10 catch err: 0 end
|
|
||||||
y = try: throw 'err' catch err: 20 end
|
|
||||||
x + y
|
|
||||||
`).toEvaluateTo(30)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('function-level exception handling', () => {
|
|
||||||
test('function with catch - no error', () => {
|
|
||||||
expect(`
|
|
||||||
read-file = do path:
|
|
||||||
path
|
|
||||||
catch e:
|
|
||||||
'default'
|
|
||||||
end
|
|
||||||
|
|
||||||
read-file test.txt
|
|
||||||
`).toEvaluateTo('test.txt')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('function with catch - error thrown', () => {
|
|
||||||
expect(`
|
|
||||||
read-file = do path:
|
|
||||||
throw 'file not found'
|
|
||||||
catch e:
|
|
||||||
'default'
|
|
||||||
end
|
|
||||||
|
|
||||||
read-file test.txt
|
|
||||||
`).toEvaluateTo('default')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('function with catch - error variable binding', () => {
|
|
||||||
expect(`
|
|
||||||
safe-call = do:
|
|
||||||
throw 'operation failed'
|
|
||||||
catch err:
|
|
||||||
err
|
|
||||||
end
|
|
||||||
|
|
||||||
safe-call
|
|
||||||
`).toEvaluateTo('operation failed')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('function with finally - always runs', () => {
|
|
||||||
expect(`
|
|
||||||
counter = 0
|
|
||||||
increment-task = do:
|
|
||||||
result = 42
|
|
||||||
result
|
|
||||||
finally:
|
|
||||||
counter = counter + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
x = increment-task
|
|
||||||
y = increment-task
|
|
||||||
counter
|
|
||||||
`).toEvaluateTo(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('function with finally - return value from body', () => {
|
|
||||||
expect(`
|
|
||||||
get-value = do:
|
|
||||||
100
|
|
||||||
finally:
|
|
||||||
999
|
|
||||||
end
|
|
||||||
|
|
||||||
get-value
|
|
||||||
`).toEvaluateTo(100)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('function with catch and finally', () => {
|
|
||||||
expect(`
|
|
||||||
cleanup-count = 0
|
|
||||||
safe-op = do should-fail:
|
|
||||||
if should-fail:
|
|
||||||
throw 'failed'
|
|
||||||
end
|
|
||||||
'success'
|
|
||||||
catch e:
|
|
||||||
'caught'
|
|
||||||
finally:
|
|
||||||
cleanup-count = cleanup-count + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
result1 = safe-op false
|
|
||||||
result2 = safe-op true
|
|
||||||
cleanup-count
|
|
||||||
`).toEvaluateTo(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('function with catch and finally - catch return value', () => {
|
|
||||||
expect(`
|
|
||||||
safe-fail = do:
|
|
||||||
throw 'always fails'
|
|
||||||
catch e:
|
|
||||||
'error handled'
|
|
||||||
finally:
|
|
||||||
noop = 1
|
|
||||||
end
|
|
||||||
|
|
||||||
safe-fail
|
|
||||||
`).toEvaluateTo('error handled')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('function without catch/finally still works', () => {
|
|
||||||
expect(`
|
|
||||||
regular = do x:
|
|
||||||
x + 10
|
|
||||||
end
|
|
||||||
|
|
||||||
regular 5
|
|
||||||
`).toEvaluateTo(15)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('nested functions with catch', () => {
|
|
||||||
expect(`
|
|
||||||
inner = do:
|
|
||||||
throw 'inner error'
|
|
||||||
catch e:
|
|
||||||
'inner caught'
|
|
||||||
end
|
|
||||||
|
|
||||||
outer = do:
|
|
||||||
inner
|
|
||||||
catch e:
|
|
||||||
'outer caught'
|
|
||||||
end
|
|
||||||
|
|
||||||
outer
|
|
||||||
`).toEvaluateTo('inner caught')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,292 +0,0 @@
|
||||||
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' })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -80,11 +80,11 @@ export const getCompoundAssignmentParts = (node: SyntaxNode) => {
|
||||||
|
|
||||||
export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
||||||
const children = getAllChildren(node)
|
const children = getAllChildren(node)
|
||||||
const [fnKeyword, paramsNode, colon, ...rest] = children
|
const [fnKeyword, paramsNode, colon, ...bodyNodes] = children
|
||||||
|
|
||||||
if (!fnKeyword || !paramsNode || !colon || !rest) {
|
if (!fnKeyword || !paramsNode || !colon || !bodyNodes) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`FunctionDef expected at least 4 children, got ${children.length}`,
|
`FunctionDef expected 5 children, got ${children.length}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to
|
node.to
|
||||||
)
|
)
|
||||||
|
|
@ -101,48 +101,8 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
||||||
return input.slice(param.from, param.to)
|
return input.slice(param.from, param.to)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Separate body nodes from catch/finally/end
|
const bodyWithoutEnd = bodyNodes.slice(0, -1)
|
||||||
const bodyNodes: SyntaxNode[] = []
|
return { paramNames, bodyNodes: bodyWithoutEnd }
|
||||||
let catchExpr: SyntaxNode | undefined
|
|
||||||
let catchVariable: string | undefined
|
|
||||||
let catchBody: SyntaxNode | undefined
|
|
||||||
let finallyExpr: SyntaxNode | undefined
|
|
||||||
let finallyBody: SyntaxNode | undefined
|
|
||||||
|
|
||||||
for (const child of rest) {
|
|
||||||
if (child.type.id === terms.CatchExpr) {
|
|
||||||
catchExpr = child
|
|
||||||
const catchChildren = getAllChildren(child)
|
|
||||||
const [_catchKeyword, identifierNode, _colon, body] = catchChildren
|
|
||||||
if (!identifierNode || !body) {
|
|
||||||
throw new CompilerError(
|
|
||||||
`CatchExpr expected identifier and body, got ${catchChildren.length} children`,
|
|
||||||
child.from,
|
|
||||||
child.to
|
|
||||||
)
|
|
||||||
}
|
|
||||||
catchVariable = input.slice(identifierNode.from, identifierNode.to)
|
|
||||||
catchBody = body
|
|
||||||
} else if (child.type.id === terms.FinallyExpr) {
|
|
||||||
finallyExpr = child
|
|
||||||
const finallyChildren = getAllChildren(child)
|
|
||||||
const [_finallyKeyword, _colon, body] = finallyChildren
|
|
||||||
if (!body) {
|
|
||||||
throw new CompilerError(
|
|
||||||
`FinallyExpr expected body, got ${finallyChildren.length} children`,
|
|
||||||
child.from,
|
|
||||||
child.to
|
|
||||||
)
|
|
||||||
}
|
|
||||||
finallyBody = body
|
|
||||||
} else if (child.type.name === 'keyword' && input.slice(child.from, child.to) === 'end') {
|
|
||||||
// Skip the end keyword
|
|
||||||
} else {
|
|
||||||
bodyNodes.push(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { paramNames, bodyNodes, catchVariable, catchBody, finallyBody }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFunctionCallParts = (node: SyntaxNode, input: string) => {
|
export const getFunctionCallParts = (node: SyntaxNode, input: string) => {
|
||||||
|
|
@ -257,12 +217,7 @@ export const getStringParts = (node: SyntaxNode, input: string) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// hasInterpolation means the string has interpolation ($var) or escape sequences (\n)
|
return { parts, hasInterpolation: parts.length > 0 }
|
||||||
// A simple string like 'hello' has one StringFragment but no interpolation
|
|
||||||
const hasInterpolation = parts.some(
|
|
||||||
(p) => p.type.id === terms.Interpolation || p.type.id === terms.EscapeSeq
|
|
||||||
)
|
|
||||||
return { parts, hasInterpolation }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
||||||
|
|
@ -297,62 +252,3 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
||||||
|
|
||||||
return { objectName, property }
|
return { objectName, property }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTryExprParts = (node: SyntaxNode, input: string) => {
|
|
||||||
const children = getAllChildren(node)
|
|
||||||
|
|
||||||
// First child is always 'try' keyword, second is colon, third is TryBlock or statement
|
|
||||||
const [tryKeyword, _colon, tryBlock, ...rest] = children
|
|
||||||
|
|
||||||
if (!tryKeyword || !tryBlock) {
|
|
||||||
throw new CompilerError(
|
|
||||||
`TryExpr expected at least 3 children, got ${children.length}`,
|
|
||||||
node.from,
|
|
||||||
node.to
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let catchExpr: SyntaxNode | undefined
|
|
||||||
let catchVariable: string | undefined
|
|
||||||
let catchBody: SyntaxNode | undefined
|
|
||||||
let finallyExpr: SyntaxNode | undefined
|
|
||||||
let finallyBody: SyntaxNode | undefined
|
|
||||||
|
|
||||||
rest.forEach((child) => {
|
|
||||||
if (child.type.id === terms.CatchExpr) {
|
|
||||||
catchExpr = child
|
|
||||||
const catchChildren = getAllChildren(child)
|
|
||||||
const [_catchKeyword, identifierNode, _colon, body] = catchChildren
|
|
||||||
if (!identifierNode || !body) {
|
|
||||||
throw new CompilerError(
|
|
||||||
`CatchExpr expected identifier and body, got ${catchChildren.length} children`,
|
|
||||||
child.from,
|
|
||||||
child.to
|
|
||||||
)
|
|
||||||
}
|
|
||||||
catchVariable = input.slice(identifierNode.from, identifierNode.to)
|
|
||||||
catchBody = body
|
|
||||||
} else if (child.type.id === terms.FinallyExpr) {
|
|
||||||
finallyExpr = child
|
|
||||||
const finallyChildren = getAllChildren(child)
|
|
||||||
const [_finallyKeyword, _colon, body] = finallyChildren
|
|
||||||
if (!body) {
|
|
||||||
throw new CompilerError(
|
|
||||||
`FinallyExpr expected body, got ${finallyChildren.length} children`,
|
|
||||||
child.from,
|
|
||||||
child.to
|
|
||||||
)
|
|
||||||
}
|
|
||||||
finallyBody = body
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
tryBlock,
|
|
||||||
catchExpr,
|
|
||||||
catchVariable,
|
|
||||||
catchBody,
|
|
||||||
finallyExpr,
|
|
||||||
finallyBody,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,6 @@ item {
|
||||||
consumeToTerminator {
|
consumeToTerminator {
|
||||||
PipeExpr |
|
PipeExpr |
|
||||||
ambiguousFunctionCall |
|
ambiguousFunctionCall |
|
||||||
TryExpr |
|
|
||||||
Throw |
|
|
||||||
IfExpr |
|
IfExpr |
|
||||||
FunctionDef |
|
FunctionDef |
|
||||||
CompoundAssign |
|
CompoundAssign |
|
||||||
|
|
@ -100,11 +98,11 @@ FunctionDef {
|
||||||
}
|
}
|
||||||
|
|
||||||
singleLineFunctionDef {
|
singleLineFunctionDef {
|
||||||
Do Params colon consumeToTerminator CatchExpr? FinallyExpr? @specialize[@name=keyword]<Identifier, "end">
|
Do Params colon consumeToTerminator @specialize[@name=keyword]<Identifier, "end">
|
||||||
}
|
}
|
||||||
|
|
||||||
multilineFunctionDef {
|
multilineFunctionDef {
|
||||||
Do Params colon newlineOrSemicolon block CatchExpr? FinallyExpr? @specialize[@name=keyword]<Identifier, "end">
|
Do Params colon newlineOrSemicolon block @specialize[@name=keyword]<Identifier, "end">
|
||||||
}
|
}
|
||||||
|
|
||||||
IfExpr {
|
IfExpr {
|
||||||
|
|
@ -132,35 +130,7 @@ ThenBlock {
|
||||||
}
|
}
|
||||||
|
|
||||||
SingleLineThenBlock {
|
SingleLineThenBlock {
|
||||||
consumeToTerminator
|
consumeToTerminator
|
||||||
}
|
|
||||||
|
|
||||||
TryExpr {
|
|
||||||
singleLineTry | multilineTry
|
|
||||||
}
|
|
||||||
|
|
||||||
singleLineTry {
|
|
||||||
@specialize[@name=keyword]<Identifier, "try"> colon consumeToTerminator CatchExpr? FinallyExpr? @specialize[@name=keyword]<Identifier, "end">
|
|
||||||
}
|
|
||||||
|
|
||||||
multilineTry {
|
|
||||||
@specialize[@name=keyword]<Identifier, "try"> colon newlineOrSemicolon TryBlock CatchExpr? FinallyExpr? @specialize[@name=keyword]<Identifier, "end">
|
|
||||||
}
|
|
||||||
|
|
||||||
CatchExpr {
|
|
||||||
@specialize[@name=keyword]<Identifier, "catch"> Identifier colon (newlineOrSemicolon TryBlock | consumeToTerminator)
|
|
||||||
}
|
|
||||||
|
|
||||||
FinallyExpr {
|
|
||||||
@specialize[@name=keyword]<Identifier, "finally"> colon (newlineOrSemicolon TryBlock | consumeToTerminator)
|
|
||||||
}
|
|
||||||
|
|
||||||
TryBlock {
|
|
||||||
block
|
|
||||||
}
|
|
||||||
|
|
||||||
Throw {
|
|
||||||
@specialize[@name=keyword]<Identifier, "throw"> (BinOp | ConditionalOp | expression)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ConditionalOp {
|
ConditionalOp {
|
||||||
|
|
|
||||||
|
|
@ -1,278 +0,0 @@
|
||||||
import { expect, describe, test } from 'bun:test'
|
|
||||||
|
|
||||||
import '../shrimp.grammar' // Importing this so changes cause it to retest!
|
|
||||||
|
|
||||||
describe('try/catch/finally/throw', () => {
|
|
||||||
test('parses try with catch', () => {
|
|
||||||
expect(`try:
|
|
||||||
risky-operation
|
|
||||||
catch err:
|
|
||||||
handle-error err
|
|
||||||
end`).toMatchTree(`
|
|
||||||
TryExpr
|
|
||||||
keyword try
|
|
||||||
colon :
|
|
||||||
TryBlock
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier risky-operation
|
|
||||||
CatchExpr
|
|
||||||
keyword catch
|
|
||||||
Identifier err
|
|
||||||
colon :
|
|
||||||
TryBlock
|
|
||||||
FunctionCall
|
|
||||||
Identifier handle-error
|
|
||||||
PositionalArg
|
|
||||||
Identifier err
|
|
||||||
keyword end
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses try with finally', () => {
|
|
||||||
expect(`try:
|
|
||||||
do-work
|
|
||||||
finally:
|
|
||||||
cleanup
|
|
||||||
end`).toMatchTree(`
|
|
||||||
TryExpr
|
|
||||||
keyword try
|
|
||||||
colon :
|
|
||||||
TryBlock
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier do-work
|
|
||||||
FinallyExpr
|
|
||||||
keyword finally
|
|
||||||
colon :
|
|
||||||
TryBlock
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier cleanup
|
|
||||||
keyword end
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses try with catch and finally', () => {
|
|
||||||
expect(`try:
|
|
||||||
risky-operation
|
|
||||||
catch err:
|
|
||||||
handle-error err
|
|
||||||
finally:
|
|
||||||
cleanup
|
|
||||||
end`).toMatchTree(`
|
|
||||||
TryExpr
|
|
||||||
keyword try
|
|
||||||
colon :
|
|
||||||
TryBlock
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier risky-operation
|
|
||||||
CatchExpr
|
|
||||||
keyword catch
|
|
||||||
Identifier err
|
|
||||||
colon :
|
|
||||||
TryBlock
|
|
||||||
FunctionCall
|
|
||||||
Identifier handle-error
|
|
||||||
PositionalArg
|
|
||||||
Identifier err
|
|
||||||
FinallyExpr
|
|
||||||
keyword finally
|
|
||||||
colon :
|
|
||||||
TryBlock
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier cleanup
|
|
||||||
keyword end
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses single-line try with catch', () => {
|
|
||||||
expect('result = try: parse-number input catch err: 0 end').toMatchTree(`
|
|
||||||
Assign
|
|
||||||
AssignableIdentifier result
|
|
||||||
Eq =
|
|
||||||
TryExpr
|
|
||||||
keyword try
|
|
||||||
colon :
|
|
||||||
FunctionCall
|
|
||||||
Identifier parse-number
|
|
||||||
PositionalArg
|
|
||||||
Identifier input
|
|
||||||
CatchExpr
|
|
||||||
keyword catch
|
|
||||||
Identifier err
|
|
||||||
colon :
|
|
||||||
Number 0
|
|
||||||
keyword end
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses single-line try with finally', () => {
|
|
||||||
expect('try: work catch err: 0 finally: cleanup end').toMatchTree(`
|
|
||||||
TryExpr
|
|
||||||
keyword try
|
|
||||||
colon :
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier work
|
|
||||||
CatchExpr
|
|
||||||
keyword catch
|
|
||||||
Identifier err
|
|
||||||
colon :
|
|
||||||
Number 0
|
|
||||||
FinallyExpr
|
|
||||||
keyword finally
|
|
||||||
colon :
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier cleanup
|
|
||||||
keyword end
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses throw statement with string', () => {
|
|
||||||
expect("throw 'error message'").toMatchTree(`
|
|
||||||
Throw
|
|
||||||
keyword throw
|
|
||||||
String
|
|
||||||
StringFragment error message
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses throw statement with identifier', () => {
|
|
||||||
expect('throw error-object').toMatchTree(`
|
|
||||||
Throw
|
|
||||||
keyword throw
|
|
||||||
Identifier error-object
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses throw statement with dict', () => {
|
|
||||||
expect('throw [type=validation-error message=failed]').toMatchTree(`
|
|
||||||
Throw
|
|
||||||
keyword throw
|
|
||||||
Dict
|
|
||||||
NamedArg
|
|
||||||
NamedArgPrefix type=
|
|
||||||
Identifier validation-error
|
|
||||||
NamedArg
|
|
||||||
NamedArgPrefix message=
|
|
||||||
Identifier failed
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not parse identifiers that start with try', () => {
|
|
||||||
expect('trying = try: work catch err: 0 end').toMatchTree(`
|
|
||||||
Assign
|
|
||||||
AssignableIdentifier trying
|
|
||||||
Eq =
|
|
||||||
TryExpr
|
|
||||||
keyword try
|
|
||||||
colon :
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier work
|
|
||||||
CatchExpr
|
|
||||||
keyword catch
|
|
||||||
Identifier err
|
|
||||||
colon :
|
|
||||||
Number 0
|
|
||||||
keyword end
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('function-level exception handling', () => {
|
|
||||||
test('parses function with catch', () => {
|
|
||||||
expect(`read-file = do path:
|
|
||||||
read-data path
|
|
||||||
catch e:
|
|
||||||
empty-string
|
|
||||||
end`).toMatchTree(`
|
|
||||||
Assign
|
|
||||||
AssignableIdentifier read-file
|
|
||||||
Eq =
|
|
||||||
FunctionDef
|
|
||||||
Do do
|
|
||||||
Params
|
|
||||||
Identifier path
|
|
||||||
colon :
|
|
||||||
FunctionCall
|
|
||||||
Identifier read-data
|
|
||||||
PositionalArg
|
|
||||||
Identifier path
|
|
||||||
CatchExpr
|
|
||||||
keyword catch
|
|
||||||
Identifier e
|
|
||||||
colon :
|
|
||||||
TryBlock
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier empty-string
|
|
||||||
keyword end
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses function with finally', () => {
|
|
||||||
expect(`cleanup-task = do x:
|
|
||||||
do-work x
|
|
||||||
finally:
|
|
||||||
close-resources
|
|
||||||
end`).toMatchTree(`
|
|
||||||
Assign
|
|
||||||
AssignableIdentifier cleanup-task
|
|
||||||
Eq =
|
|
||||||
FunctionDef
|
|
||||||
Do do
|
|
||||||
Params
|
|
||||||
Identifier x
|
|
||||||
colon :
|
|
||||||
FunctionCall
|
|
||||||
Identifier do-work
|
|
||||||
PositionalArg
|
|
||||||
Identifier x
|
|
||||||
FinallyExpr
|
|
||||||
keyword finally
|
|
||||||
colon :
|
|
||||||
TryBlock
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier close-resources
|
|
||||||
keyword end
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses function with catch and finally', () => {
|
|
||||||
expect(`safe-operation = do x:
|
|
||||||
risky-work x
|
|
||||||
catch err:
|
|
||||||
log err
|
|
||||||
default-value
|
|
||||||
finally:
|
|
||||||
cleanup
|
|
||||||
end`).toMatchTree(`
|
|
||||||
Assign
|
|
||||||
AssignableIdentifier safe-operation
|
|
||||||
Eq =
|
|
||||||
FunctionDef
|
|
||||||
Do do
|
|
||||||
Params
|
|
||||||
Identifier x
|
|
||||||
colon :
|
|
||||||
FunctionCall
|
|
||||||
Identifier risky-work
|
|
||||||
PositionalArg
|
|
||||||
Identifier x
|
|
||||||
CatchExpr
|
|
||||||
keyword catch
|
|
||||||
Identifier err
|
|
||||||
colon :
|
|
||||||
TryBlock
|
|
||||||
FunctionCall
|
|
||||||
Identifier log
|
|
||||||
PositionalArg
|
|
||||||
Identifier err
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier default-value
|
|
||||||
FinallyExpr
|
|
||||||
keyword finally
|
|
||||||
colon :
|
|
||||||
TryBlock
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier cleanup
|
|
||||||
keyword end
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -39,7 +39,7 @@ export const globals = {
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case 'string': case 'array': return value.value.length
|
case 'string': case 'array': return value.value.length
|
||||||
case 'dict': return value.value.size
|
case 'dict': return value.value.size
|
||||||
default: throw new Error(`length: expected string, array, or dict, got ${value.type}`)
|
default: return 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -65,24 +65,7 @@ export const globals = {
|
||||||
identity: (v: any) => v,
|
identity: (v: any) => v,
|
||||||
|
|
||||||
// collections
|
// collections
|
||||||
at: (collection: any, index: number | string) => {
|
at: (collection: any, index: number | string) => collection[index],
|
||||||
const value = toValue(collection)
|
|
||||||
if (value.type === 'string' || value.type === 'array') {
|
|
||||||
const idx = typeof index === 'number' ? index : parseInt(index as string)
|
|
||||||
if (idx < 0 || idx >= value.value.length) {
|
|
||||||
throw new Error(`at: index ${idx} out of bounds for ${value.type} of length ${value.value.length}`)
|
|
||||||
}
|
|
||||||
return value.value[idx]
|
|
||||||
} else if (value.type === 'dict') {
|
|
||||||
const key = String(index)
|
|
||||||
if (!value.value.has(key)) {
|
|
||||||
throw new Error(`at: key '${key}' not found in dict`)
|
|
||||||
}
|
|
||||||
return value.value.get(key)
|
|
||||||
} else {
|
|
||||||
throw new Error(`at: expected string, array, or dict, got ${value.type}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
range: (start: number, end: number | null) => {
|
range: (start: number, end: number | null) => {
|
||||||
if (end === null) {
|
if (end === null) {
|
||||||
end = start
|
end = start
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { type Value, toValue, toNull } from 'reefvm'
|
|
||||||
|
|
||||||
export const list = {
|
export const list = {
|
||||||
slice: (list: any[], start: number, end?: number) => list.slice(start, end),
|
slice: (list: any[], start: number, end?: number) => list.slice(start, end),
|
||||||
map: async (list: any[], cb: Function) => {
|
map: async (list: any[], cb: Function) => {
|
||||||
|
|
@ -42,41 +40,9 @@ export const list = {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
// mutating
|
|
||||||
push: (list: Value, item: Value) => {
|
|
||||||
if (list.type !== 'array') return toNull()
|
|
||||||
return toValue(list.value.push(item))
|
|
||||||
},
|
|
||||||
pop: (list: Value) => {
|
|
||||||
if (list.type !== 'array') return toNull()
|
|
||||||
return toValue(list.value.pop())
|
|
||||||
},
|
|
||||||
shift: (list: Value) => {
|
|
||||||
if (list.type !== 'array') return toNull()
|
|
||||||
return toValue(list.value.shift())
|
|
||||||
},
|
|
||||||
unshift: (list: Value, item: Value) => {
|
|
||||||
if (list.type !== 'array') return toNull()
|
|
||||||
return toValue(list.value.unshift(item))
|
|
||||||
},
|
|
||||||
splice: (list: Value, start: Value, deleteCount: Value, ...items: Value[]) => {
|
|
||||||
const realList = list.value as any[]
|
|
||||||
const realStart = start.value as number
|
|
||||||
const realDeleteCount = deleteCount.value as number
|
|
||||||
const realItems = items.map(item => item.value)
|
|
||||||
return toValue(realList.splice(realStart, realDeleteCount, ...realItems))
|
|
||||||
},
|
|
||||||
|
|
||||||
// sequence operations
|
// sequence operations
|
||||||
reverse: (list: any[]) => list.slice().reverse(),
|
reverse: (list: any[]) => list.slice().reverse(),
|
||||||
sort: async (list: any[], cb?: (a: any, b: any) => number) => {
|
sort: (list: any[], cb?: (a: any, b: any) => number) => list.slice().sort(cb),
|
||||||
const arr = [...list]
|
|
||||||
if (!cb) return arr.sort()
|
|
||||||
for (let i = 0; i < arr.length; i++)
|
|
||||||
for (let j = i + 1; j < arr.length; j++)
|
|
||||||
if ((await cb(arr[i], arr[j])) > 0) [arr[i], arr[j]] = [arr[j], arr[i]]
|
|
||||||
return arr
|
|
||||||
},
|
|
||||||
concat: (...lists: any[][]) => lists.flat(1),
|
concat: (...lists: any[][]) => lists.flat(1),
|
||||||
flatten: (list: any[], depth: number = 1) => list.flat(depth),
|
flatten: (list: any[], depth: number = 1) => list.flat(depth),
|
||||||
unique: (list: any[]) => Array.from(new Set(list)),
|
unique: (list: any[]) => Array.from(new Set(list)),
|
||||||
|
|
@ -86,14 +52,8 @@ export const list = {
|
||||||
first: (list: any[]) => list[0] ?? null,
|
first: (list: any[]) => list[0] ?? null,
|
||||||
last: (list: any[]) => list[list.length - 1] ?? null,
|
last: (list: any[]) => list[list.length - 1] ?? null,
|
||||||
rest: (list: any[]) => list.slice(1),
|
rest: (list: any[]) => list.slice(1),
|
||||||
take: (list: any[], n: number) => {
|
take: (list: any[], n: number) => list.slice(0, n),
|
||||||
if (n < 0) throw new Error(`take: count must be non-negative, got ${n}`)
|
drop: (list: any[], n: number) => list.slice(n),
|
||||||
return list.slice(0, n)
|
|
||||||
},
|
|
||||||
drop: (list: any[], n: number) => {
|
|
||||||
if (n < 0) throw new Error(`drop: count must be non-negative, got ${n}`)
|
|
||||||
return list.slice(n)
|
|
||||||
},
|
|
||||||
append: (list: any[], item: any) => [...list, item],
|
append: (list: any[], item: any) => [...list, item],
|
||||||
prepend: (list: any[], item: any) => [item, ...list],
|
prepend: (list: any[], item: any) => [item, ...list],
|
||||||
'index-of': (list: any[], item: any) => list.indexOf(item),
|
'index-of': (list: any[], item: any) => list.indexOf(item),
|
||||||
|
|
@ -126,13 +86,4 @@ export const list = {
|
||||||
}
|
}
|
||||||
return groups
|
return groups
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// raw functions deal directly in Value types, meaning we can modify collection
|
|
||||||
// careful - they MUST return a Value!
|
|
||||||
; (list.splice as any).raw = true
|
|
||||||
; (list.push as any).raw = true
|
|
||||||
; (list.pop as any).raw = true
|
|
||||||
; (list.shift as any).raw = true
|
|
||||||
; (list.unshift as any).raw = true
|
|
||||||
|
|
@ -3,24 +3,12 @@ export const math = {
|
||||||
floor: (n: number) => Math.floor(n),
|
floor: (n: number) => Math.floor(n),
|
||||||
ceil: (n: number) => Math.ceil(n),
|
ceil: (n: number) => Math.ceil(n),
|
||||||
round: (n: number) => Math.round(n),
|
round: (n: number) => Math.round(n),
|
||||||
min: (...nums: number[]) => {
|
min: (...nums: number[]) => Math.min(...nums),
|
||||||
if (nums.length === 0) throw new Error('min: expected at least one argument')
|
max: (...nums: number[]) => Math.max(...nums),
|
||||||
return Math.min(...nums)
|
|
||||||
},
|
|
||||||
max: (...nums: number[]) => {
|
|
||||||
if (nums.length === 0) throw new Error('max: expected at least one argument')
|
|
||||||
return Math.max(...nums)
|
|
||||||
},
|
|
||||||
pow: (base: number, exp: number) => Math.pow(base, exp),
|
pow: (base: number, exp: number) => Math.pow(base, exp),
|
||||||
sqrt: (n: number) => {
|
sqrt: (n: number) => Math.sqrt(n),
|
||||||
if (n < 0) throw new Error(`sqrt: cannot take square root of negative number ${n}`)
|
|
||||||
return Math.sqrt(n)
|
|
||||||
},
|
|
||||||
random: () => Math.random(),
|
random: () => Math.random(),
|
||||||
clamp: (n: number, min: number, max: number) => {
|
clamp: (n: number, min: number, max: number) => Math.min(Math.max(n, min), max),
|
||||||
if (min > max) throw new Error(`clamp: min (${min}) must be less than or equal to max (${max})`)
|
|
||||||
return Math.min(Math.max(n, min), max)
|
|
||||||
},
|
|
||||||
sign: (n: number) => Math.sign(n),
|
sign: (n: number) => Math.sign(n),
|
||||||
trunc: (n: number) => Math.trunc(n),
|
trunc: (n: number) => Math.trunc(n),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,7 @@ export const str = {
|
||||||
'replace-all': (str: string, search: string, replacement: string) => str.replaceAll(search, replacement),
|
'replace-all': (str: string, search: string, replacement: string) => str.replaceAll(search, replacement),
|
||||||
slice: (str: string, start: number, end?: number | null) => str.slice(start, end ?? undefined),
|
slice: (str: string, start: number, end?: number | null) => str.slice(start, end ?? undefined),
|
||||||
substring: (str: string, start: number, end?: number | null) => str.substring(start, end ?? undefined),
|
substring: (str: string, start: number, end?: number | null) => str.substring(start, end ?? undefined),
|
||||||
repeat: (str: string, count: number) => {
|
repeat: (str: string, count: number) => str.repeat(count),
|
||||||
if (count < 0) throw new Error(`repeat: count must be non-negative, got ${count}`)
|
|
||||||
if (!Number.isInteger(count)) throw new Error(`repeat: count must be an integer, got ${count}`)
|
|
||||||
return str.repeat(count)
|
|
||||||
},
|
|
||||||
'pad-start': (str: string, length: number, pad: string = ' ') => str.padStart(length, pad),
|
'pad-start': (str: string, length: number, pad: string = ' ') => str.padStart(length, pad),
|
||||||
'pad-end': (str: string, length: number, pad: string = ' ') => str.padEnd(length, pad),
|
'pad-end': (str: string, length: number, pad: string = ' ') => str.padEnd(length, pad),
|
||||||
lines: (str: string) => str.split('\n'),
|
lines: (str: string) => str.split('\n'),
|
||||||
|
|
|
||||||
|
|
@ -176,12 +176,9 @@ describe('introspection', () => {
|
||||||
await expect(`length 'hello'`).toEvaluateTo(5, globals)
|
await expect(`length 'hello'`).toEvaluateTo(5, globals)
|
||||||
await expect(`length [1 2 3]`).toEvaluateTo(3, globals)
|
await expect(`length [1 2 3]`).toEvaluateTo(3, globals)
|
||||||
await expect(`length [a=1 b=2]`).toEvaluateTo(2, globals)
|
await expect(`length [a=1 b=2]`).toEvaluateTo(2, globals)
|
||||||
})
|
await expect(`length 42`).toEvaluateTo(0, globals)
|
||||||
|
await expect(`length true`).toEvaluateTo(0, globals)
|
||||||
test('length throws on invalid types', async () => {
|
await expect(`length null`).toEvaluateTo(0, globals)
|
||||||
await expect(`try: length 42 catch e: 'error' end`).toEvaluateTo('error', globals)
|
|
||||||
await expect(`try: length true catch e: 'error' end`).toEvaluateTo('error', globals)
|
|
||||||
await expect(`try: length null catch e: 'error' end`).toEvaluateTo('error', globals)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('inspect formats values', async () => {
|
test('inspect formats values', async () => {
|
||||||
|
|
@ -344,84 +341,6 @@ describe('collections', () => {
|
||||||
await expect(`list.index-of [1 2 3] 5`).toEvaluateTo(-1, globals)
|
await expect(`list.index-of [1 2 3] 5`).toEvaluateTo(-1, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.push adds to end and mutates array', async () => {
|
|
||||||
await expect(`arr = [1 2]; list.push arr 3; arr`).toEvaluateTo([1, 2, 3], globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.push returns the size of the array', async () => {
|
|
||||||
await expect(`arr = [1 2]; arr | list.push 3`).toEvaluateTo(3, globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.pop removes from end and mutates array', async () => {
|
|
||||||
await expect(`arr = [1 2 3]; list.pop arr; arr`).toEvaluateTo([1, 2], globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.pop returns removed element', async () => {
|
|
||||||
await expect(`list.pop [1 2 3]`).toEvaluateTo(3, globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.pop returns null for empty array', async () => {
|
|
||||||
await expect(`list.pop []`).toEvaluateTo(null, globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.shift removes from start and mutates array', async () => {
|
|
||||||
await expect(`arr = [1 2 3]; list.shift arr; arr`).toEvaluateTo([2, 3], globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.shift returns removed element', async () => {
|
|
||||||
await expect(`list.shift [1 2 3]`).toEvaluateTo(1, globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.shift returns null for empty array', async () => {
|
|
||||||
await expect(`list.shift []`).toEvaluateTo(null, globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.unshift adds to start and mutates array', async () => {
|
|
||||||
await expect(`arr = [2 3]; list.unshift arr 1; arr`).toEvaluateTo([1, 2, 3], globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.unshift returns the length of the array', async () => {
|
|
||||||
await expect(`arr = [2 3]; arr | list.unshift 1`).toEvaluateTo(3, globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.splice removes elements and mutates array', async () => {
|
|
||||||
await expect(`arr = [1 2 3 4 5]; list.splice arr 1 2; arr`).toEvaluateTo([1, 4, 5], globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.splice returns removed elements', async () => {
|
|
||||||
await expect(`list.splice [1 2 3 4 5] 1 2`).toEvaluateTo([2, 3], globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.splice from start', async () => {
|
|
||||||
await expect(`list.splice [1 2 3 4 5] 0 2`).toEvaluateTo([1, 2], globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.splice to end', async () => {
|
|
||||||
await expect(`arr = [1 2 3 4 5]; list.splice arr 3 2; arr`).toEvaluateTo([1, 2, 3], globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.sort with no callback sorts ascending', async () => {
|
|
||||||
await expect(`list.sort [3 1 4 1 5] null`).toEvaluateTo([1, 1, 3, 4, 5], globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.sort with callback sorts using comparator', async () => {
|
|
||||||
await expect(`
|
|
||||||
desc = do a b:
|
|
||||||
b - a
|
|
||||||
end
|
|
||||||
list.sort [3 1 4 1 5] desc
|
|
||||||
`).toEvaluateTo([5, 4, 3, 1, 1], globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.sort with callback for strings by length', async () => {
|
|
||||||
await expect(`
|
|
||||||
by-length = do a b:
|
|
||||||
(length a) - (length b)
|
|
||||||
end
|
|
||||||
list.sort ['cat' 'a' 'dog' 'elephant'] by-length
|
|
||||||
`).toEvaluateTo(['a', 'cat', 'dog', 'elephant'], globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.any? checks if any element matches', async () => {
|
test('list.any? checks if any element matches', async () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
gt-three = do x: x > 3 end
|
gt-three = do x: x > 3 end
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user