Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a93bf4ba4 | |||
| f81a9669cf | |||
| fa0b377eca | |||
| bc62987abb | |||
| e60e3184fa | |||
| f8d2236292 | |||
| 4f961d3039 | |||
| d957675ac8 | |||
| 9bc514a782 | |||
| 701ca98401 |
26
bin/shrimp
26
bin/shrimp
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
import { Compiler } from '../src/compiler/compiler'
|
import { Compiler } from '../src/compiler/compiler'
|
||||||
import { colors, globals } from '../src/prelude'
|
import { colors, globals } from '../src/prelude'
|
||||||
|
import { parser } from '../src/parser/shrimp'
|
||||||
|
import { treeToString } from '../src/utils/tree'
|
||||||
import { VM, fromValue, bytecodeToString } from 'reefvm'
|
import { VM, fromValue, bytecodeToString } from 'reefvm'
|
||||||
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
||||||
import { randomUUID } from "crypto"
|
import { randomUUID } from 'crypto'
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
|
|
@ -32,6 +34,17 @@ async function compileFile(filePath: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function parseFile(filePath: string) {
|
||||||
|
try {
|
||||||
|
const code = readFileSync(filePath, 'utf-8')
|
||||||
|
const tree = parser.parse(code)
|
||||||
|
return treeToString(tree, code)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`${colors.red}Error:${colors.reset} ${error.message}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showHelp() {
|
function showHelp() {
|
||||||
console.log(`${colors.bright}${colors.magenta}🦐 Shrimp${colors.reset} is a scripting language in a shell.
|
console.log(`${colors.bright}${colors.magenta}🦐 Shrimp${colors.reset} is a scripting language in a shell.
|
||||||
|
|
||||||
|
|
@ -39,6 +52,7 @@ ${colors.bright}Usage:${colors.reset} shrimp <command> [...args]
|
||||||
|
|
||||||
${colors.bright}Commands:${colors.reset}
|
${colors.bright}Commands:${colors.reset}
|
||||||
${colors.cyan}run ${colors.yellow}./my-file.sh${colors.reset} Execute a file with Shrimp
|
${colors.cyan}run ${colors.yellow}./my-file.sh${colors.reset} Execute a file with Shrimp
|
||||||
|
${colors.cyan}parse ${colors.yellow}./my-file.sh${colors.reset} Print parse tree for Shrimp file
|
||||||
${colors.cyan}bytecode ${colors.yellow}./my-file.sh${colors.reset} Print bytecode for Shrimp file
|
${colors.cyan}bytecode ${colors.yellow}./my-file.sh${colors.reset} Print bytecode for Shrimp file
|
||||||
${colors.cyan}eval ${colors.yellow}'some code'${colors.reset} Evaluate a line of Shrimp code
|
${colors.cyan}eval ${colors.yellow}'some code'${colors.reset} Evaluate a line of Shrimp code
|
||||||
${colors.cyan}repl${colors.reset} Start REPL
|
${colors.cyan}repl${colors.reset} Start REPL
|
||||||
|
|
@ -102,6 +116,16 @@ async function main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['parse', '-parse', '--parse', '-p'].includes(command)) {
|
||||||
|
const file = args[1]
|
||||||
|
if (!file) {
|
||||||
|
console.log(`${colors.bright}usage: shrimp parse <file>${colors.reset}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
console.log(await parseFile(file))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (['run', '-run', '--run', '-r'].includes(command)) {
|
if (['run', '-run', '--run', '-r'].includes(command)) {
|
||||||
const file = args[1]
|
const file = args[1]
|
||||||
if (!file) {
|
if (!file) {
|
||||||
|
|
|
||||||
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#4b2fd615546cc4dd1cacd40ce3cf4c014d3eec9f", { "peerDependencies": { "typescript": "^5" } }, "4b2fd615546cc4dd1cacd40ce3cf4c014d3eec9f"],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
getNamedArgParts,
|
getNamedArgParts,
|
||||||
getPipeExprParts,
|
getPipeExprParts,
|
||||||
getStringParts,
|
getStringParts,
|
||||||
|
getTryExprParts,
|
||||||
} from '#compiler/utils'
|
} from '#compiler/utils'
|
||||||
|
|
||||||
const DEBUG = false
|
const DEBUG = false
|
||||||
|
|
@ -51,6 +52,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -273,7 +275,10 @@ export class Compiler {
|
||||||
}
|
}
|
||||||
|
|
||||||
case terms.FunctionDef: {
|
case terms.FunctionDef: {
|
||||||
const { paramNames, bodyNodes } = getFunctionDefParts(node, input)
|
const { paramNames, bodyNodes, catchVariable, catchBody, finallyBody } = getFunctionDefParts(
|
||||||
|
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}`
|
||||||
|
|
@ -281,9 +286,27 @@ export class Compiler {
|
||||||
instructions.push(['JUMP', afterLabel])
|
instructions.push(['JUMP', afterLabel])
|
||||||
|
|
||||||
instructions.push([`${functionLabel}:`])
|
instructions.push([`${functionLabel}:`])
|
||||||
bodyNodes.forEach((bodyNode) => {
|
|
||||||
instructions.push(...this.#compileNode(bodyNode, input))
|
const compileFunctionBody = () => {
|
||||||
})
|
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}:`])
|
||||||
|
|
@ -315,8 +338,20 @@ export class Compiler {
|
||||||
CALL
|
CALL
|
||||||
*/
|
*/
|
||||||
case terms.FunctionCall: {
|
case terms.FunctionCall: {
|
||||||
const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts(node, input)
|
const { identifierNode, namedArgs, positionalArgs, bang } = getFunctionCallParts(node, input)
|
||||||
const instructions: ProgramItem[] = []
|
const instructions: ProgramItem[] = []
|
||||||
|
|
||||||
|
let catchLabel = ''
|
||||||
|
let endLabel = ''
|
||||||
|
|
||||||
|
if (bang) {
|
||||||
|
// wrap function call in try block
|
||||||
|
this.tryLabelCount++
|
||||||
|
catchLabel = `.catch_${this.tryLabelCount}`
|
||||||
|
endLabel = `.end_try_${this.tryLabelCount}`
|
||||||
|
instructions.push(['PUSH_TRY', catchLabel])
|
||||||
|
}
|
||||||
|
|
||||||
instructions.push(...this.#compileNode(identifierNode, input))
|
instructions.push(...this.#compileNode(identifierNode, input))
|
||||||
|
|
||||||
positionalArgs.forEach((arg) => {
|
positionalArgs.forEach((arg) => {
|
||||||
|
|
@ -333,14 +368,66 @@ export class Compiler {
|
||||||
instructions.push(['PUSH', namedArgs.length])
|
instructions.push(['PUSH', namedArgs.length])
|
||||||
instructions.push(['CALL'])
|
instructions.push(['CALL'])
|
||||||
|
|
||||||
|
if (bang) {
|
||||||
|
instructions.push(['PUSH', null])
|
||||||
|
instructions.push(['SWAP'])
|
||||||
|
instructions.push(['MAKE_ARRAY', 2])
|
||||||
|
instructions.push(['POP_TRY'])
|
||||||
|
instructions.push(['JUMP', endLabel])
|
||||||
|
|
||||||
|
instructions.push([`${catchLabel}:`])
|
||||||
|
instructions.push(['PUSH', null])
|
||||||
|
instructions.push(['MAKE_ARRAY', 2])
|
||||||
|
|
||||||
|
instructions.push([`${endLabel}:`])
|
||||||
|
}
|
||||||
|
|
||||||
return instructions
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
||||||
case terms.ThenBlock:
|
case terms.ThenBlock:
|
||||||
case terms.SingleLineThenBlock: {
|
case terms.SingleLineThenBlock:
|
||||||
const instructions = getAllChildren(node)
|
case terms.TryBlock: {
|
||||||
.map((child) => this.#compileNode(child, input))
|
const children = getAllChildren(node)
|
||||||
.flat()
|
const instructions: ProgramItem[] = []
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
@ -547,4 +634,52 @@ 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,73 @@ describe('native functions', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('error handling with ! suffix', () => {
|
||||||
|
test('function with ! suffix returns [null, result] on success', () => {
|
||||||
|
const readFile = () => 'file contents'
|
||||||
|
expect(`[ error content ] = read-file! test.txt; error`).toEvaluateTo(null, { 'read-file': readFile })
|
||||||
|
expect(`[ error content ] = read-file! test.txt; content`).toEvaluateTo('file contents', { 'read-file': readFile })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('function with ! suffix returns [error, null] on failure', () => {
|
||||||
|
const readFile = () => { throw new Error('File not found') }
|
||||||
|
expect(`[ error content ] = read-file! test.txt; error`).toEvaluateTo('File not found', { 'read-file': readFile })
|
||||||
|
expect(`[ error content ] = read-file! test.txt; content`).toEvaluateTo(null, { 'read-file': readFile })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can use error in conditional', () => {
|
||||||
|
const readFile = () => { throw new Error('Not found') }
|
||||||
|
expect(`
|
||||||
|
[ error content ] = read-file! test.txt
|
||||||
|
if error:
|
||||||
|
'failed'
|
||||||
|
else:
|
||||||
|
content
|
||||||
|
end
|
||||||
|
`).toEvaluateTo('failed', { 'read-file': readFile })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('successful result in conditional', () => {
|
||||||
|
const readFile = () => 'success data'
|
||||||
|
expect(`
|
||||||
|
[ error content ] = read-file! test.txt
|
||||||
|
if error:
|
||||||
|
'failed'
|
||||||
|
else:
|
||||||
|
content
|
||||||
|
end
|
||||||
|
`).toEvaluateTo('success data', { 'read-file': readFile })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('function without ! suffix throws normally', () => {
|
||||||
|
const readFile = () => { throw new Error('Normal error') }
|
||||||
|
expect(`read-file test.txt`).toFailEvaluation({ 'read-file': readFile })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can destructure and use both values', () => {
|
||||||
|
const parseJson = (json: string) => JSON.parse(json)
|
||||||
|
expect(`
|
||||||
|
[ error result ] = parse-json! '{"a": 1}'
|
||||||
|
if error:
|
||||||
|
null
|
||||||
|
else:
|
||||||
|
result.a
|
||||||
|
end
|
||||||
|
`).toEvaluateTo(1, { 'parse-json': parseJson })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can destructure with invalid json', () => {
|
||||||
|
const parseJson = (json: string) => JSON.parse(json)
|
||||||
|
expect(`
|
||||||
|
[ error result ] = parse-json! 'invalid'
|
||||||
|
if error:
|
||||||
|
'parse error'
|
||||||
|
else:
|
||||||
|
result
|
||||||
|
end
|
||||||
|
`).toEvaluateTo('parse error', { 'parse-json': parseJson })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('dot get', () => {
|
describe('dot get', () => {
|
||||||
const array = (...items: any) => items
|
const array = (...items: any) => items
|
||||||
const dict = (atNamed: any) => atNamed
|
const dict = (atNamed: any) => atNamed
|
||||||
|
|
|
||||||
311
src/compiler/tests/exceptions.test.ts
Normal file
311
src/compiler/tests/exceptions.test.ts
Normal file
|
|
@ -0,0 +1,311 @@
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
292
src/compiler/tests/native-exceptions.test.ts
Normal file
292
src/compiler/tests/native-exceptions.test.ts
Normal file
|
|
@ -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 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' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -67,11 +67,11 @@ export const getAssignmentParts = (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, ...bodyNodes] = children
|
const [fnKeyword, paramsNode, colon, ...rest] = children
|
||||||
|
|
||||||
if (!fnKeyword || !paramsNode || !colon || !bodyNodes) {
|
if (!fnKeyword || !paramsNode || !colon || !rest) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`FunctionDef expected 5 children, got ${children.length}`,
|
`FunctionDef expected at least 4 children, got ${children.length}`,
|
||||||
node.from,
|
node.from,
|
||||||
node.to
|
node.to
|
||||||
)
|
)
|
||||||
|
|
@ -88,17 +88,63 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
|
||||||
return input.slice(param.from, param.to)
|
return input.slice(param.from, param.to)
|
||||||
})
|
})
|
||||||
|
|
||||||
const bodyWithoutEnd = bodyNodes.slice(0, -1)
|
// Separate body nodes from catch/finally/end
|
||||||
return { paramNames, bodyNodes: bodyWithoutEnd }
|
const bodyNodes: SyntaxNode[] = []
|
||||||
|
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) => {
|
||||||
const [identifierNode, ...args] = getAllChildren(node)
|
const [identifierNode, ...args] = getAllChildren(node)
|
||||||
|
let bang = false
|
||||||
|
|
||||||
if (!identifierNode) {
|
if (!identifierNode) {
|
||||||
throw new CompilerError(`FunctionCall expected at least 1 child, got 0`, node.from, node.to)
|
throw new CompilerError(`FunctionCall expected at least 1 child, got 0`, node.from, node.to)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.length > 0 && args[0]?.type.id === terms.Bang) {
|
||||||
|
bang = true
|
||||||
|
args.shift()
|
||||||
|
}
|
||||||
|
|
||||||
const namedArgs = args.filter((arg) => arg.type.id === terms.NamedArg)
|
const namedArgs = args.filter((arg) => arg.type.id === terms.NamedArg)
|
||||||
const positionalArgs = args
|
const positionalArgs = args
|
||||||
.filter((arg) => arg.type.id === terms.PositionalArg)
|
.filter((arg) => arg.type.id === terms.PositionalArg)
|
||||||
|
|
@ -109,7 +155,7 @@ export const getFunctionCallParts = (node: SyntaxNode, input: string) => {
|
||||||
return child
|
return child
|
||||||
})
|
})
|
||||||
|
|
||||||
return { identifierNode, namedArgs, positionalArgs }
|
return { identifierNode, namedArgs, positionalArgs, bang }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNamedArgParts = (node: SyntaxNode, input: string) => {
|
export const getNamedArgParts = (node: SyntaxNode, input: string) => {
|
||||||
|
|
@ -204,7 +250,12 @@ export const getStringParts = (node: SyntaxNode, input: string) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return { parts, hasInterpolation: parts.length > 0 }
|
// hasInterpolation means the string has interpolation ($var) or escape sequences (\n)
|
||||||
|
// 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) => {
|
||||||
|
|
@ -239,3 +290,62 @@ 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { ContextTracker, InputStream } from '@lezer/lr'
|
||||||
import * as terms from './shrimp.terms'
|
import * as terms from './shrimp.terms'
|
||||||
|
|
||||||
export class Scope {
|
export class Scope {
|
||||||
constructor(public parent: Scope | null, public vars = new Set<string>()) {}
|
constructor(public parent: Scope | null, public vars = new Set<string>()) { }
|
||||||
|
|
||||||
has(name: string): boolean {
|
has(name: string): boolean {
|
||||||
return this.vars.has(name) || (this.parent?.has(name) ?? false)
|
return this.vars.has(name) || (this.parent?.has(name) ?? false)
|
||||||
|
|
@ -42,7 +42,7 @@ export class Scope {
|
||||||
|
|
||||||
// Tracker context that combines Scope with temporary pending identifiers
|
// Tracker context that combines Scope with temporary pending identifiers
|
||||||
class TrackerContext {
|
class TrackerContext {
|
||||||
constructor(public scope: Scope, public pendingIds: string[] = []) {}
|
constructor(public scope: Scope, public pendingIds: string[] = []) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract identifier text from input stream
|
// Extract identifier text from input stream
|
||||||
|
|
@ -75,6 +75,12 @@ export const trackScope = new ContextTracker<TrackerContext>({
|
||||||
return new TrackerContext(context.scope, [...context.pendingIds, text])
|
return new TrackerContext(context.scope, [...context.pendingIds, text])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track identifiers in array destructuring: [ a b ] = ...
|
||||||
|
if (!inParams && term === terms.Identifier && isArrayDestructuring(input)) {
|
||||||
|
const text = readIdentifierText(input, input.pos, stack.pos)
|
||||||
|
return new TrackerContext(Scope.add(context.scope, text), context.pendingIds)
|
||||||
|
}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -98,3 +104,26 @@ export const trackScope = new ContextTracker<TrackerContext>({
|
||||||
|
|
||||||
hash: (context) => context.scope.hash(),
|
hash: (context) => context.scope.hash(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Check if we're parsing array destructuring: [ a b ] = ...
|
||||||
|
const isArrayDestructuring = (input: InputStream): boolean => {
|
||||||
|
let pos = 0
|
||||||
|
|
||||||
|
// Find closing bracket
|
||||||
|
while (pos < 200 && input.peek(pos) !== 93 /* ] */) {
|
||||||
|
if (input.peek(pos) === -1) return false // EOF
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.peek(pos) !== 93 /* ] */) return false
|
||||||
|
pos++
|
||||||
|
|
||||||
|
// Skip whitespace
|
||||||
|
while (input.peek(pos) === 32 /* space */ ||
|
||||||
|
input.peek(pos) === 9 /* tab */ ||
|
||||||
|
input.peek(pos) === 10 /* \n */) {
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
|
||||||
|
return input.peek(pos) === 61 /* = */
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
comment { "#" ![\n]* }
|
comment { "#" ![\n]* }
|
||||||
leftParen { "(" }
|
leftParen { "(" }
|
||||||
rightParen { ")" }
|
rightParen { ")" }
|
||||||
|
Bang { "!" }
|
||||||
colon[closedBy="end", @name="colon"] { ":" }
|
colon[closedBy="end", @name="colon"] { ":" }
|
||||||
Underscore { "_" }
|
Underscore { "_" }
|
||||||
Regex { "//" (![/\\\n[] | "\\" ![\n] | "[" (![\n\\\]] | "\\" ![\n])* "]")+ ("//" $[gimsuy]*)? } // Stolen from the lezer JavaScript grammar
|
Regex { "//" (![/\\\n[] | "\\" ![\n] | "[" (![\n\\\]] | "\\" ![\n])* "]")+ ("//" $[gimsuy]*)? } // Stolen from the lezer JavaScript grammar
|
||||||
|
|
@ -51,6 +52,8 @@ item {
|
||||||
consumeToTerminator {
|
consumeToTerminator {
|
||||||
PipeExpr |
|
PipeExpr |
|
||||||
ambiguousFunctionCall |
|
ambiguousFunctionCall |
|
||||||
|
TryExpr |
|
||||||
|
Throw |
|
||||||
IfExpr |
|
IfExpr |
|
||||||
FunctionDef |
|
FunctionDef |
|
||||||
Assign |
|
Assign |
|
||||||
|
|
@ -68,7 +71,7 @@ pipeOperand {
|
||||||
}
|
}
|
||||||
|
|
||||||
FunctionCallOrIdentifier {
|
FunctionCallOrIdentifier {
|
||||||
DotGet | Identifier
|
(DotGet | Identifier) Bang?
|
||||||
}
|
}
|
||||||
|
|
||||||
ambiguousFunctionCall {
|
ambiguousFunctionCall {
|
||||||
|
|
@ -76,7 +79,7 @@ ambiguousFunctionCall {
|
||||||
}
|
}
|
||||||
|
|
||||||
FunctionCall {
|
FunctionCall {
|
||||||
(DotGet | Identifier | ParenExpr) arg+
|
(DotGet | Identifier | ParenExpr) Bang? arg+
|
||||||
}
|
}
|
||||||
|
|
||||||
arg {
|
arg {
|
||||||
|
|
@ -97,11 +100,11 @@ FunctionDef {
|
||||||
}
|
}
|
||||||
|
|
||||||
singleLineFunctionDef {
|
singleLineFunctionDef {
|
||||||
Do Params colon consumeToTerminator @specialize[@name=keyword]<Identifier, "end">
|
Do Params colon consumeToTerminator CatchExpr? FinallyExpr? @specialize[@name=keyword]<Identifier, "end">
|
||||||
}
|
}
|
||||||
|
|
||||||
multilineFunctionDef {
|
multilineFunctionDef {
|
||||||
Do Params colon newlineOrSemicolon block @specialize[@name=keyword]<Identifier, "end">
|
Do Params colon newlineOrSemicolon block CatchExpr? FinallyExpr? @specialize[@name=keyword]<Identifier, "end">
|
||||||
}
|
}
|
||||||
|
|
||||||
IfExpr {
|
IfExpr {
|
||||||
|
|
@ -132,6 +135,34 @@ 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 {
|
||||||
expression !comparison EqEq expression |
|
expression !comparison EqEq expression |
|
||||||
expression !comparison Neq expression |
|
expression !comparison Neq expression |
|
||||||
|
|
|
||||||
|
|
@ -26,28 +26,34 @@ export const
|
||||||
Number = 24,
|
Number = 24,
|
||||||
ParenExpr = 25,
|
ParenExpr = 25,
|
||||||
FunctionCallOrIdentifier = 26,
|
FunctionCallOrIdentifier = 26,
|
||||||
BinOp = 27,
|
Bang = 27,
|
||||||
String = 28,
|
BinOp = 28,
|
||||||
StringFragment = 29,
|
String = 29,
|
||||||
Interpolation = 30,
|
StringFragment = 30,
|
||||||
EscapeSeq = 31,
|
Interpolation = 31,
|
||||||
Boolean = 32,
|
EscapeSeq = 32,
|
||||||
Regex = 33,
|
Boolean = 33,
|
||||||
Dict = 34,
|
Regex = 34,
|
||||||
NamedArg = 35,
|
Dict = 35,
|
||||||
NamedArgPrefix = 36,
|
NamedArg = 36,
|
||||||
FunctionDef = 37,
|
NamedArgPrefix = 37,
|
||||||
Params = 38,
|
FunctionDef = 38,
|
||||||
colon = 39,
|
Params = 39,
|
||||||
keyword = 54,
|
colon = 40,
|
||||||
Underscore = 41,
|
CatchExpr = 41,
|
||||||
Array = 42,
|
keyword = 64,
|
||||||
Null = 43,
|
TryBlock = 43,
|
||||||
ConditionalOp = 44,
|
FinallyExpr = 44,
|
||||||
PositionalArg = 45,
|
Underscore = 47,
|
||||||
IfExpr = 47,
|
Array = 48,
|
||||||
SingleLineThenBlock = 49,
|
Null = 49,
|
||||||
ThenBlock = 50,
|
ConditionalOp = 50,
|
||||||
ElseIfExpr = 51,
|
PositionalArg = 51,
|
||||||
ElseExpr = 53,
|
TryExpr = 53,
|
||||||
Assign = 55
|
Throw = 55,
|
||||||
|
IfExpr = 57,
|
||||||
|
SingleLineThenBlock = 59,
|
||||||
|
ThenBlock = 60,
|
||||||
|
ElseIfExpr = 61,
|
||||||
|
ElseExpr = 63,
|
||||||
|
Assign = 65
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,24 @@ import {operatorTokenizer} from "./operatorTokenizer"
|
||||||
import {tokenizer, specializeKeyword} from "./tokenizer"
|
import {tokenizer, specializeKeyword} from "./tokenizer"
|
||||||
import {trackScope} from "./scopeTracker"
|
import {trackScope} from "./scopeTracker"
|
||||||
import {highlighting} from "./highlight"
|
import {highlighting} from "./highlight"
|
||||||
const spec_Identifier = {__proto__:null,end:80, null:86, if:96, elseif:104, else:108}
|
const spec_Identifier = {__proto__:null,catch:84, finally:90, end:92, null:98, try:108, throw:112, if:116, elseif:124, else:128}
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "3[QYQbOOO#hQcO'#CvO$eOSO'#CxO$sQbO'#EVOOQ`'#DR'#DROOQa'#DO'#DOO%vQbO'#DWO'RQcO'#DzOOQa'#Dz'#DzO(UQcO'#DzO)WQcO'#DyO*PQRO'#CwO*dQcO'#DuO*{QcO'#DuO+^QbO'#CuO,UOpO'#CsOOQ`'#Dv'#DvO,ZQbO'#DuO,iQbO'#E]OOQ`'#D]'#D]O-^QRO'#DeOOQ`'#Du'#DuO-cQQO'#DtOOQ`'#Dt'#DtOOQ`'#Df'#DfQYQbOOO-kQbO'#DPOOQa'#Dy'#DyOOQ`'#DZ'#DZOOQ`'#E['#E[OOQ`'#Dm'#DmO-uQbO,59^O.cQbO'#CzO.kQWO'#C{OOOO'#D|'#D|OOOO'#Dg'#DgO/POSO,59dOOQa,59d,59dOOQ`'#Di'#DiO/_QbO'#DSO/gQQO,5:qOOQ`'#Dh'#DhO/lQbO,59rO/sQQO,59jOOQa,59r,59rO0OQbO,59rO0YQbO,5:PO,iQbO,59cO,iQbO,59cO,iQbO,59cO,iQbO,59tO,iQbO,59tO,iQbO,59tO0dQRO,59aO0kQRO,59aO0|QRO,59aO0wQQO,59aO1XQQO,59aO1aObO,59_O1lQbO'#DnO1wQbO,59]O2YQRO,5:wO2aQRO,5:wOOQ`,5:`,5:`OOQ`-E7d-E7dOOQ`,59k,59kOOQ`-E7k-E7kOOOO,59f,59fOOOO,59g,59gOOOO-E7e-E7eOOQa1G/O1G/OOOQ`-E7g-E7gO2lQbO1G0]OOQ`-E7f-E7fO2yQQO1G/UOOQa1G/^1G/^O3UQbO1G/^OOQO'#Dk'#DkO2yQQO1G/UOOQa1G/U1G/UOOQ`'#Dl'#DlO3UQbO1G/^OOQ`1G/k1G/kOOQa1G.}1G.}O3wQcO1G.}O4RQcO1G.}O4]QcO1G.}OOQa1G/`1G/`O5oQcO1G/`O5vQcO1G/`O5}QcO1G/`OOQa1G.{1G.{OOQa1G.y1G.yO!ZQbO'#CvO6UQbO'#CrOOQ`,5:Y,5:YOOQ`-E7l-E7lO6cQbO1G0cO6pQbO7+%wO6uQbO7+%xO7VQQO7+$pOOQa7+$p7+$pO7bQbO7+$xOOQa7+$x7+$xOOQO-E7i-E7iOOQ`-E7j-E7jOOQ`'#D_'#D_O7lQbO7+%}O7qQbO7+&OOOQ`<<Ic<<IcOOQ`'#Dj'#DjO8XQQO'#DjO8^QbO'#EXO8tQbO<<IdOOQa<<H[<<H[OOQa<<Hd<<HdOOQ`<<Ii<<IiOOQ`'#D`'#D`O8yQbO<<IjOOQ`,5:U,5:UOOQ`-E7h-E7hOOQ`AN?OAN?OO,iQbO'#DaOOQ`'#Do'#DoO9UQbOAN?UO9aQQO'#DcOOQ`AN?UAN?UO9fQbOAN?UO9kQRO,59{O9rQRO,59{OOQ`-E7m-E7mOOQ`G24pG24pO9}QbOG24pO:SQQO,59}O:XQQO1G/gOOQ`LD*[LD*[O6uQbO1G/iO7qQbO7+%ROOQ`7+%T7+%TOOQ`<<Hm<<Hm",
|
states: "9bQYQbOOO#wQcO'#CvO$tOSO'#CyO%SQbO'#EaOOQ`'#DS'#DSOOQa'#DP'#DPO&VQbO'#D^O'hQcO'#EUOOQa'#EU'#EUO(nQcO'#EUO)pQcO'#ETO*TQRO'#CxO+aQcO'#EPO+qQcO'#EPO+{QbO'#CuO,sOpO'#CsOOQ`'#EQ'#EQO,xQbO'#EPO-PQQO'#EgOOQ`'#Dc'#DcO-UQbO'#DeO-UQbO'#EiOOQ`'#Dg'#DgO-yQRO'#DoOOQ`'#EP'#EPO.OQQO'#EOOOQ`'#EO'#EOOOQ`'#Dp'#DpQYQbOOO.WQbO,59bO.zQbO'#DQOOQa'#ET'#ETOOQ`'#Da'#DaOOQ`'#Ef'#EfOOQ`'#Dw'#DwO/UQbO,59^O/xQbO'#C{O0QQWO'#C|OOOO'#EW'#EWOOOO'#Dq'#DqO0fOSO,59eOOQa,59e,59eOOQ`'#Ds'#DsO0tQbO'#DTO0|QQO,5:{OOQ`'#Dr'#DrO1RQbO,59xO1YQQO,59kOOQa,59x,59xO1eQbO,59xO1oQbO,59^O1|QbO,5:ZO-UQbO,59dO-UQbO,59dO-UQbO,59dO-UQbO,59zO-UQbO,59zO-UQbO,59zO2^QRO,59aO2eQRO,59aO2vQRO,59aO2qQQO,59aO3RQQO,59aO3ZObO,59_O3fQbO'#DxO3qQbO,59]O4YQbO,5;RO4mQcO,5:PO5cQcO,5:PO5sQcO,5:PO6iQRO,5;TO6pQRO,5;TOOQ`,5:j,5:jOOQ`-E7n-E7nO6{QbO1G.xOOQ`,59l,59lOOQ`-E7u-E7uOOOO,59g,59gOOOO,59h,59hOOOO-E7o-E7oOOQa1G/P1G/POOQ`-E7q-E7qO7oQbO1G0gOOQ`-E7p-E7pO8SQQO1G/VOOQa1G/d1G/dO8_QbO1G/dOOQO'#Du'#DuO8SQQO1G/VOOQa1G/V1G/VOOQ`'#Dv'#DvO8_QbO1G/dOOQ`1G/u1G/uOOQa1G/O1G/OO9WQcO1G/OO9bQcO1G/OO9lQcO1G/OOOQa1G/f1G/fO;[QcO1G/fO;cQcO1G/fO;jQcO1G/fOOQa1G.{1G.{OOQa1G.y1G.yO!aQbO'#CvO;qQbO'#CrOOQ`,5:d,5:dOOQ`-E7v-E7vO<RQbO1G0mO<^QbO1G0nO<zQbO1G0oO=_QbO7+&RO<^QbO7+&TO=jQQO7+$qOOQa7+$q7+$qO=uQbO7+%OOOQa7+%O7+%OOOQO-E7s-E7sOOQ`-E7t-E7tO>PQbO'#DVO>UQQO'#DYOOQ`7+&X7+&XO>ZQbO7+&XO>`QbO7+&XOOQ`'#Dt'#DtO>hQQO'#DtO>mQbO'#EbOOQ`'#DX'#DXO?aQbO7+&YOOQ`'#Di'#DiO?lQbO7+&ZO?qQbO7+&[OOQ`<<Im<<ImO@_QbO<<ImO@dQbO<<ImO@lQbO<<IoOOQa<<H]<<H]OOQa<<Hj<<HjO@wQQO,59qO@|QbO,59tOOQ`<<Is<<IsOAaQbO<<IsOOQ`,5:`,5:`OOQ`-E7r-E7rOOQ`<<It<<ItOAfQbO<<ItOAkQbO<<ItOOQ`<<Iu<<IuOOQ`'#Dj'#DjOAsQbO<<IvOOQ`AN?XAN?XOBOQbOAN?XOOQ`AN?ZAN?ZOBTQbOAN?ZOBYQbOAN?ZOBbQbO1G/]OBuQbO1G/`OOQ`1G/`1G/`OOQ`AN?_AN?_OOQ`AN?`AN?`OC]QbOAN?`O-UQbO'#DkOOQ`'#Dy'#DyOCbQbOAN?bOCmQQO'#DmOOQ`AN?bAN?bOCrQbOAN?bOOQ`G24sG24sOOQ`G24uG24uOCwQbOG24uOC|QbO7+$wOOQ`7+$w7+$wOOQ`7+$z7+$zOOQ`G24zG24zODgQRO,5:VODnQRO,5:VOOQ`-E7w-E7wOOQ`G24|G24|ODyQbOG24|OEOQQO,5:XOOQ`LD*aLD*aOOQ`<<Hc<<HcOETQQO1G/qOOQ`LD*hLD*hOBuQbO1G/sO?qQbO7+%]OOQ`7+%_7+%_OOQ`<<Hw<<Hw",
|
||||||
stateData: ":a~O!fOS!gOS~O_PO`dOaWOb_OcROhWOpWOqWO{WO!QbO!l^O!oQO!vTO!wUO!xgO~O_kOaWOb_OcROhWOpWOqWOtjOylO{WO!l^O!oQO!vTO!wUO!OjX!xjX#RjX!}jXxjX~OP!mXQ!mXR!mXS!mXT!mXU!mXW!mXX!mXY!mXZ!mX[!mX]!mX^!mX~P!ZOmrO!ouO!qpO!rqO~O_vOwvP~O_kOaWOb_OhWOpWOqWOtjO{WO!l^O!oQO!vTO!wUO!xyO~O!||O~P${OP!nXQ!nXR!nXS!nXT!nXU!nXW!nXX!nXY!nXZ!nX[!nX]!nX^!nX!x!nX#R!nXx!nX~O_kOaWOb_OcROhWOpWOqWOtjOylO{WO!l^O!oQO!vTO!wUO!}!nX~P%}OV!OO~P%}OP!mXQ!mXR!mXS!mXT!mXU!mXW!mXX!mXY!mXZ!mX[!mX]!mX^!mX~O!x!iX#R!iXx!iX~P(]OT!TOU!UOW!SOX!SOY!SOZ!SO[!SO]!SO~OP!QOQ!QOR!ROS!RO^!PO~P)eOP!QOQ!QOR!ROS!RO!x!iX#R!iXx!iX~OT!TOU!UO!x!iX#R!iXx!iX~O_POaWOb_OcROhWOpWOqWO{WO!l^O!oQO!vTO!wUO~O!k![O~O!O!]O!x!iX#R!iXx!iX~O_kOaWOb_OhWOpWOqWO{WO!l^O!oQO!vTO!wUO~OV!OO~O!x!aO#R!aO~OcROy!cO~P,iOcROtjOylO!Ofa!xfa#Rfa!}faxfa~P,iO_!eO!l^O~O!o!fO!q!fO!r!fO!s!fO!t!fO!u!fO~OmrO!o!hO!qpO!rqO~O_vOwvX~Ow!jO~O!|!mO~P${OtjO!x!oO!|!qO~O!x!rO!|!mO~P,iO`dO!QbO~P+^O!}!}O~P(]OP!QOQ!QOR!ROS!RO!}!}O~OT!TOU!UO!}!}O~O!O!]O!}!}O~O_#OOh#OO!l^O~O_#POb_O!l^O~O!O!]O!xea#Rea!}eaxea~Ow#TO~P)eOT!TOU!UOw#TO~O`dO!QbO!x#VO~P+^OtjO!x!oO!|#XO~O!x!rO!|#ZO~P,iO^!PORkiSki!xki#Rki!}kixki~OPkiQki~P3`OP!QOQ!QO~P3`OP!QOQ!QORkiSki!xki#Rki!}kixki~OW!SOX!SOY!SOZ!SO[!SO]!SOT|i!x|i#R|i!}|iw|ix|i~OU!UO~P4wOU!UO~P5ZOU|i~P4wOcROtjOylO~P,iO`dO!QbO!x#`O~P+^Ox#aO~O`dO!QbO!x#bOx!{P~P+^OtjO!x!oO!|#fO~O!x!rO!|#gO~P,iOx#hO~O`dO!QbO!x#bOx!{P!U!{P!W!{P~P+^O!x#kO~O`dO!QbO!x#bOx!{X!U!{X!W!{X~P+^Ox#mO~Ox#rO!U#nO!W#qO~Ox#wO!U#nO!W#qO~Ow#yO~Ox#wO~Ow#zO~P)eOT!TOU!UOw#zO~Ox#{O~O!x#|O~O!x#}O~Ohq~",
|
stateData: "E]~O!pOS!qOS~O_PO`gOaWOb_OcROhWOqWOrWO!RWO!WbO!YdO![eO!v^O!yQO#QTO#RUO#SjO~O_oOaWOb_OcROhWOkmOqWOrWOunO!PpO!RWO!v^O!yQO#QTO#RUO!UjX#SjX#_jX#XjXzjX}jX!OjX~OP!wXQ!wXR!wXS!wXT!wXU!wXW!wXX!wXY!wXZ!wX[!wX]!wX^!wX~P!aOnvO!yyO!{tO!|uO~O_zOxwP~O_oOaWOb_OhWOqWOrWOunO!RWO!v^O!yQO#QTO#RUO#S}O~O#W!QO~P%[OP!xXQ!xXR!xXS!xXT!xXU!xXW!xXX!xXY!xXZ!xX[!xX]!xX^!xX#S!xX#_!xXz!xX}!xX!O!xX~O_oOaWOb_OcROhWOk!SOqWOrWOunO!PpO!RWO!v^O!yQO#QTO#RUO#X!xX~P&^OV!TO~P&^OP!wXQ!wXR!wXS!wXT!wXU!wXW!wXX!wXY!wXZ!wX[!wX]!wX^!wX~O#S!sX#_!sXz!sX}!sX!O!sX~P(uOP!VOQ!VOR!WOS!WOT!YOU!ZOW!XOX!XOY!XOZ!XO[!XO]!XO^!UO~O#S!sX#_!sXz!sX}!sX!O!sX~OP!VOQ!VOR!WOS!WO~P+OOT!YOU!ZO~P+OO_POaWOb_OcROhWOqWOrWO!RWO!v^O!yQO#QTO#RUO~O!u!aO~O!U!bO~P+OOx!dO~O_oOaWOb_OhWOqWOrWO!RWO!v^O!yQO#QTO#RUO~OV!TO~O#S!jO#_!jO~OcROunO!PpO!Uja#Sja#_ja#Xjazja}ja!Oja~P-UOcRO!P!mO~P-UOcROunO!PpO!Ufa#Sfa#_fa#Xfazfa}fa!Ofa~P-UO_!oO!v^O~O!y!pO!{!pO!|!pO!}!pO#O!pO#P!pO~OnvO!y!rO!{tO!|uO~O_zOxwX~Ox!tO~O#W!wO~P%[OunO#S!yO#W!{O~O#S!|O#W!wO~P-UOcROunO!PpO~P-UO`gO!WbO!YdO![eO~P+{O#X#XO~P(uOP!VOQ!VOR!WOS!WO#X#XO~OT!YOU!ZO#X#XO~O!U!bO#X#XO~O_#YOh#YO!v^O~O_#ZOb_O!v^O~O!U!bO#Sea#_ea#Xeazea}ea!Oea~O`gO!WbO!YdO![eO#S#`O~P+{O#S!Xa#_!Xaz!Xa}!Xa!O!Xa~P*TO#S!Xa#_!Xaz!Xa}!Xa!O!Xa~OP!VOQ!VOR!WOS!WO~P5QOT!YOU!ZO~P5QOT!YOU!ZOW!XOX!XOY!XOZ!XO[!XO]!XO~Ox#aO~P5}OT!YOU!ZOx#aO~OcROunO!PpO!Ufi#Sfi#_fi#Xfizfi}fi!Ofi~P-UO`gO!WbO!YdO![eO#S#cO~P+{OunO#S!yO#W#eO~O#S!|O#W#gO~P-UO^!UORliSli#Sli#_li#Xlizli}li!Oli~OPliQli~P8iOP!VOQ!VO~P8iOP!VOQ!VORliSli#Sli#_li#Xlizli}li!Oli~OW!XOX!XOY!XOZ!XO[!XO]!XOT!Si#S!Si#_!Si#X!Six!Siz!Si}!Si!O!Si~OU!ZO~P:^OU!ZO~P:pOU!Si~P:^OcROk!SOunO!PpO~P-UOz#jO}#kO!O#lO~O`gO!WbO!YdO![eO#S#oOz#UP}#UP!O#UP~P+{O`gO!WbO!YdO![eO#S#vO~P+{Oz#jO}#kO!O#wO~OunO#S!yO#W#{O~O#S!|O#W#|O~P-UO_#}O~Ox$OO~O!O$PO~O}#kO!O$PO~O#S$RO~O`gO!WbO!YdO![eO#S#oOz#UX}#UX!O#UX!`#UX!b#UX~P+{Oz#jO}#kO!O$TO~O!O$WO~O`gO!WbO!YdO![eO#S#oO!O#UP!`#UP!b#UP~P+{O!O$ZO~O}#kO!O$ZO~Oz#jO}#kO!O$]O~Ox$`O~O`gO!WbO!YdO![eO#S$aO~P+{O!O$cO~O!O$dO~O}#kO!O$dO~O!O$jO!`$fO!b$iO~O!O$lO~O!O$mO~O}#kO!O$mO~O`gO!WbO!YdO![eO#S$oO~P+{O`gO!WbO!YdO![eO#S#oO!O#UP~P+{O!O$rO~O!O$vO!`$fO!b$iO~Ox$xO~O!O$vO~O!O$yO~O`gO!WbO!YdO![eO#S#oO}#UP!O#UP~P+{Ox${O~P5}OT!YOU!ZOx${O~O!O$|O~O#S$}O~O#S%OO~Ohr~",
|
||||||
goto: "/r#RPPPPPPPPPPPPPPPPPPPPP#S#c#qP$i#c%g%|P&o&oPP%|&sP'W'qPPP'tP(i)UP)]P)i)l)uP)yP)]*P*V*]*c*i*r*|+W+a+hPPPP+n+r,WPP,j-wP.nPPPPPPPP.r.r/VPP/_/f/fdeOi!O!j#T#V#`#d#|#}R!Y^i`O^i!O!]!j#T#V#`#d#|#}fPO^i!O!j#T#V#`#d#|#}xkPUVbjoz}!P!Q!R!S!T!U!n!s#P#Q#Y#nR#P!]fVO^i!O!j#T#V#`#d#|#}xWPUVbjoz}!P!Q!R!S!T!U!n!s#P#Q#Y#nQ!epQ#O![R#Q!]d[Oi!O!j#T#V#`#d#|#}Q!W^Q!u!QR!x!R!aWOPUV^bijoz}!O!P!Q!R!S!T!U!j!n!s#P#Q#T#V#Y#`#d#n#|#}TrQtYmPVo#P#QQ{UQ!lzX!o{!l!p#WdeOi!O!j#T#V#`#d#|#}YlPVo#P#QQ!Y^R!cjRxRzWPUV^bjoz}!P!Q!R!S!T!U!n!s#P#Q#Y#neXOi!O!j#T#V#`#d#|#}d]Oi!O!j#T#V#`#d#|#}Q!X^Q!`bQ!y!UQ!{!TR#u#nZmPVo#P#QeeOi!O!j#T#V#`#d#|#}R#_#TQ#j#`Q$O#|R$P#}T#o#j#pQ#s#jR#x#pQiOR!biQtQR!gtQzUR!kzQwRR!iwW#d#V#`#|#}R#l#dQ!p{Q#W!lT#[!p#WQ!s}Q#Y!nT#]!s#YWoPV#P#QR!doS!^a!ZR#S!^Q#p#jR#v#pThOiSfOiQ!t!OQ#U!jQ#^#TZ#c#V#`#d#|#}daOi!O!j#T#V#`#d#|#}Q!Z^R#R!]fZO^i!O!j#T#V#`#d#|#}YlPVo#P#QQ}UQ!_bQ!cjQ!nzW!r}!n!s#YQ!u!PQ!v!QQ!w!RQ!y!SQ!z!TQ!|!UR#t#ndYOi!O!j#T#V#`#d#|#}xkPUVbjoz}!P!Q!R!S!T!U!n!s#P#Q#Y#nR!V^TsQtsSOPV^ijo!O!j#P#Q#T#V#`#d#|#}Q#e#VV#i#`#|#}ZnPVo#P#QecOi!O!j#T#V#`#d#|#}",
|
goto: "4y#_PPPPPPPPPPPPPPPPPPPPP#`#u$ZP%]#uP&e'TP(Q(QPP'T(UP(l)`P)cP)o)xPPP*bP+a,VP,aP,aP,aP,s,v-PP-TP,a-Z-a-g-m-s.P.Z.e.s.zPPPP/Q/U/vPP0`1yP2zPPPPPPPP3O3l3OPP3y4T4T4g4gphOl!T!d!t#`#a#c#q#v$O$`$a$o$}%OR!_^u`O^l!T!b!d!t#`#a#c#q#v$O$`$a$o$}%OrPO^l!T!d!t#`#a#c#q#v$O$`$a$o$}%O!QoPUVdemns!O!R!S!U!V!W!X!Y!Z!l!x!}#Z#[#f$fR#Z!brVO^l!T!d!t#`#a#c#q#v$O$`$a$o$}%O!QWPUVdemns!O!R!S!U!V!W!X!Y!Z!l!x!}#Z#[#f$fQ!otQ#Y!aR#[!bp[Ol!T!d!t#`#a#c#q#v$O$`$a$o$}%OQ!]^Q!fdQ#P!VR#S!W!uWOPUV^delmns!O!R!S!T!U!V!W!X!Y!Z!d!l!t!x!}#Z#[#`#a#c#f#q#v$O$`$a$f$o$}%OTvQx`qPVms!S!l#Z#[Q!PUQ!v!OX!y!P!v!z#dphOl!T!d!t#`#a#c#q#v$O$`$a$o$}%O`pPVms!S!l#Z#[Q!_^R!mnR|RQ#n#_Q#y#bQ$V#sR$_#zQ#s#`Q$q$aR$z$oQ#m#_Q#x#bQ$Q#nQ$U#sQ$[#yQ$^#zQ$e$VR$n$_!SWPUV^demns!O!R!S!U!V!W!X!Y!Z!l!x!}#Z#[#f$fqXOl!T!d!t#`#a#c#q#v$O$`$a$o$}%Op]Ol!T!d!t#`#a#c#q#v$O$`$a$o$}%OQ!^^Q!gdQ!ieQ#T!ZQ#V!YR$t$faqPVms!S!l#Z#[qhOl!T!d!t#`#a#c#q#v$O$`$a$o$}%OR#u#aQ$Y#vQ%P$}R%Q%OT$g$Y$hQ$k$YR$w$hQlOR!klQxQR!qxQ!OUR!u!OQ{RR!s{^#q#`#c#v$a$o$}%OR$S#qQ!z!PQ#d!vT#h!z#dQ!}!RQ#f!xT#i!}#fWsPV#Z#[S!lm!ST!ns!lS!ca!`R#^!cQ$h$YR$u$hTkOlSiOlQ#O!TQ#_!dQ#b!t`#p#`#c#q#v$a$o$}%OQ#t#aQ$b$OR$p$`paOl!T!d!t#`#a#c#q#v$O$`$a$o$}%OQ!`^R#]!brZO^l!T!d!t#`#a#c#q#v$O$`$a$o$}%O`pPVms!S!l#Z#[Q!RUQ!edQ!heQ!mnQ!x!OW!|!R!x!}#fQ#P!UQ#Q!VQ#R!WQ#T!XQ#U!YQ#W!ZR$s$fpYOl!T!d!t#`#a#c#q#v$O$`$a$o$}%O!QoPUVdemns!O!R!S!U!V!W!X!Y!Z!l!x!}#Z#[#f$fR![^TwQx!VSOPV^lmns!S!T!d!l!t#Z#[#`#a#c#q#v$O$`$a$o$}%OU#r#`$a$oQ#z#cV$X#v$}%OarPVms!S!l#Z#[qcOl!T!d!t#`#a#c#q#v$O$`$a$o$}%OqfOl!T!d!t#`#a#c#q#v$O$`$a$o$}%O",
|
||||||
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Dict NamedArg NamedArgPrefix FunctionDef Params colon keyword Underscore Array Null ConditionalOp PositionalArg operator IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
|
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier Bang BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Dict NamedArg NamedArgPrefix FunctionDef Params colon CatchExpr keyword TryBlock FinallyExpr keyword keyword Underscore Array Null ConditionalOp PositionalArg operator TryExpr keyword Throw keyword IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
|
||||||
maxTerm: 95,
|
maxTerm: 107,
|
||||||
context: trackScope,
|
context: trackScope,
|
||||||
nodeProps: [
|
nodeProps: [
|
||||||
["closedBy", 39,"end"]
|
["closedBy", 40,"end"]
|
||||||
],
|
],
|
||||||
propSources: [highlighting],
|
propSources: [highlighting],
|
||||||
skippedNodes: [0],
|
skippedNodes: [0],
|
||||||
repeatNodeCount: 10,
|
repeatNodeCount: 10,
|
||||||
tokenData: "AO~R|OX#{XY$jYZ%TZp#{pq$jqs#{st%ntu'Vuw#{wx'[xy'ayz'zz{#{{|(e|}#{}!O(e!O!P#{!P!Q+X!Q![)S![!]3t!]!^%T!^!}#{!}#O4_#O#P6T#P#Q6Y#Q#R#{#R#S6s#S#T#{#T#Y7^#Y#Z8l#Z#b7^#b#c<z#c#f7^#f#g=q#g#h7^#h#i>h#i#o7^#o#p#{#p#q@`#q;'S#{;'S;=`$d<%l~#{~O#{~~@yS$QUmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUmS!fYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UmS!xQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZmS!gYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!gYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O!q~~'aO!o~U'hUmS!lQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUmS!}QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWmSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZYmShQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OWmSOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oWmShQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^WmSOt#{uw#{x!P#{!P!Q+v!Q#O#{#P;'S#{;'S;=`$d<%lO#{U+{^mSOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q#{!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wU-O^mSqQOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q0o!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wQ.PXqQOY-zZ!P-z!P!Q.l!Q!}-z!}#O/Z#O#P0Y#P;'S-z;'S;=`0i<%lO-zQ.oP!P!Q.rQ.wUqQ#Z#[.r#]#^.r#a#b.r#g#h.r#i#j.r#m#n.rQ/^VOY/ZZ#O/Z#O#P/s#P#Q-z#Q;'S/Z;'S;=`0S<%lO/ZQ/vSOY/ZZ;'S/Z;'S;=`0S<%lO/ZQ0VP;=`<%l/ZQ0]SOY-zZ;'S-z;'S;=`0i<%lO-zQ0lP;=`<%l-zU0tWmSOt#{uw#{x!P#{!P!Q1^!Q#O#{#P;'S#{;'S;=`$d<%lO#{U1ebmSqQOt#{uw#{x#O#{#P#Z#{#Z#[1^#[#]#{#]#^1^#^#a#{#a#b1^#b#g#{#g#h1^#h#i#{#i#j1^#j#m#{#m#n1^#n;'S#{;'S;=`$d<%lO#{U2r[mSOY2mYZ#{Zt2mtu/Zuw2mwx/Zx#O2m#O#P/s#P#Q,w#Q;'S2m;'S;=`3h<%lO2mU3kP;=`<%l2mU3qP;=`<%l,wU3{UmSwQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4fW!wQmSOt#{uw#{x!_#{!_!`5O!`#O#{#P;'S#{;'S;=`$d<%lO#{U5TVmSOt#{uw#{x#O#{#P#Q5j#Q;'S#{;'S;=`$d<%lO#{U5qU!vQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6YO!r~U6aU!|QmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6zUmSyQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7cYmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{U8YUtQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U8qZmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#U9d#U#o7^#o;'S#{;'S;=`$d<%lO#{U9i[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#`7^#`#a:_#a#o7^#o;'S#{;'S;=`$d<%lO#{U:d[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#g7^#g#h;Y#h#o7^#o;'S#{;'S;=`$d<%lO#{U;_[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#X7^#X#Y<T#Y#o7^#o;'S#{;'S;=`$d<%lO#{U<[YpQmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=RY!sWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=xY!uWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^>o[!tWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#f7^#f#g?e#g#o7^#o;'S#{;'S;=`$d<%lO#{U?j[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#i7^#i#j;Y#j#o7^#o;'S#{;'S;=`$d<%lO#{U@gU!OQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~AOO#R~",
|
tokenData: "Al~R}OX$OXY$mYZ%WZp$Opq$mqr%qrs$Ost&[tu'suw$Owx'xxy'}yz(hz{$O{|)R|}$O}!O)R!O!P$O!P!Q+u!Q![)p![!]4b!]!^%W!^!}$O!}#O4{#O#P6q#P#Q6v#Q#R$O#R#S7a#S#T$O#T#Y7z#Y#Z9Y#Z#b7z#b#c=h#c#f7z#f#g>_#g#h7z#h#i?U#i#o7z#o#p$O#p#q@|#q;'S$O;'S;=`$g<%l~$O~O$O~~AgS$TUnSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OS$jP;=`<%l$O^$tUnS!pYOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU%_UnS#SQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU%xUkQnSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O^&cZnS!qYOY&[YZ$OZt&[tu'Uuw&[wx'Ux#O&[#O#P'U#P;'S&[;'S;=`'m<%lO&[Y'ZS!qYOY'UZ;'S'U;'S;=`'g<%lO'UY'jP;=`<%l'U^'pP;=`<%l&[~'xO!{~~'}O!y~U(UUnS!vQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU(oUnS#XQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU)WWnSOt$Ouw$Ox!Q$O!Q![)p![#O$O#P;'S$O;'S;=`$g<%lO$OU)wYnShQOt$Ouw$Ox!O$O!O!P*g!P!Q$O!Q![)p![#O$O#P;'S$O;'S;=`$g<%lO$OU*lWnSOt$Ouw$Ox!Q$O!Q![+U![#O$O#P;'S$O;'S;=`$g<%lO$OU+]WnShQOt$Ouw$Ox!Q$O!Q![+U![#O$O#P;'S$O;'S;=`$g<%lO$OU+zWnSOt$Ouw$Ox!P$O!P!Q,d!Q#O$O#P;'S$O;'S;=`$g<%lO$OU,i^nSOY-eYZ$OZt-etu.huw-ewx.hx!P-e!P!Q$O!Q!}-e!}#O3Z#O#P0v#P;'S-e;'S;=`4[<%lO-eU-l^nSrQOY-eYZ$OZt-etu.huw-ewx.hx!P-e!P!Q1]!Q!}-e!}#O3Z#O#P0v#P;'S-e;'S;=`4[<%lO-eQ.mXrQOY.hZ!P.h!P!Q/Y!Q!}.h!}#O/w#O#P0v#P;'S.h;'S;=`1V<%lO.hQ/]P!P!Q/`Q/eUrQ#Z#[/`#]#^/`#a#b/`#g#h/`#i#j/`#m#n/`Q/zVOY/wZ#O/w#O#P0a#P#Q.h#Q;'S/w;'S;=`0p<%lO/wQ0dSOY/wZ;'S/w;'S;=`0p<%lO/wQ0sP;=`<%l/wQ0ySOY.hZ;'S.h;'S;=`1V<%lO.hQ1YP;=`<%l.hU1bWnSOt$Ouw$Ox!P$O!P!Q1z!Q#O$O#P;'S$O;'S;=`$g<%lO$OU2RbnSrQOt$Ouw$Ox#O$O#P#Z$O#Z#[1z#[#]$O#]#^1z#^#a$O#a#b1z#b#g$O#g#h1z#h#i$O#i#j1z#j#m$O#m#n1z#n;'S$O;'S;=`$g<%lO$OU3`[nSOY3ZYZ$OZt3Ztu/wuw3Zwx/wx#O3Z#O#P0a#P#Q-e#Q;'S3Z;'S;=`4U<%lO3ZU4XP;=`<%l3ZU4_P;=`<%l-eU4iUnSxQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU5SW#RQnSOt$Ouw$Ox!_$O!_!`5l!`#O$O#P;'S$O;'S;=`$g<%lO$OU5qVnSOt$Ouw$Ox#O$O#P#Q6W#Q;'S$O;'S;=`$g<%lO$OU6_U#QQnSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O~6vO!|~U6}U#WQnSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU7hUnS!PQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU8PYnSOt$Ouw$Ox!_$O!_!`8o!`#O$O#P#T$O#T#o7z#o;'S$O;'S;=`$g<%lO$OU8vUuQnSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU9_ZnSOt$Ouw$Ox!_$O!_!`8o!`#O$O#P#T$O#T#U:Q#U#o7z#o;'S$O;'S;=`$g<%lO$OU:V[nSOt$Ouw$Ox!_$O!_!`8o!`#O$O#P#T$O#T#`7z#`#a:{#a#o7z#o;'S$O;'S;=`$g<%lO$OU;Q[nSOt$Ouw$Ox!_$O!_!`8o!`#O$O#P#T$O#T#g7z#g#h;v#h#o7z#o;'S$O;'S;=`$g<%lO$OU;{[nSOt$Ouw$Ox!_$O!_!`8o!`#O$O#P#T$O#T#X7z#X#Y<q#Y#o7z#o;'S$O;'S;=`$g<%lO$OU<xYqQnSOt$Ouw$Ox!_$O!_!`8o!`#O$O#P#T$O#T#o7z#o;'S$O;'S;=`$g<%lO$O^=oY!}WnSOt$Ouw$Ox!_$O!_!`8o!`#O$O#P#T$O#T#o7z#o;'S$O;'S;=`$g<%lO$O^>fY#PWnSOt$Ouw$Ox!_$O!_!`8o!`#O$O#P#T$O#T#o7z#o;'S$O;'S;=`$g<%lO$O^?][#OWnSOt$Ouw$Ox!_$O!_!`8o!`#O$O#P#T$O#T#f7z#f#g@R#g#o7z#o;'S$O;'S;=`$g<%lO$OU@W[nSOt$Ouw$Ox!_$O!_!`8o!`#O$O#P#T$O#T#i7z#i#j;v#j#o7z#o;'S$O;'S;=`$g<%lO$OUATU!UQnSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O~AlO#_~",
|
||||||
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!k~~", 11)],
|
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!u~~", 11)],
|
||||||
topRules: {"Program":[0,20]},
|
topRules: {"Program":[0,20]},
|
||||||
specialized: [{term: 15, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 15, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
|
specialized: [{term: 15, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 15, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
|
||||||
tokenPrec: 1164
|
tokenPrec: 1666
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,91 @@ describe('Identifier', () => {
|
||||||
Identifier even?`)
|
Identifier even?`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('parses bang as postfix operator on function calls', () => {
|
||||||
|
expect('read-file! test.txt').toMatchTree(`
|
||||||
|
FunctionCall
|
||||||
|
Identifier read-file
|
||||||
|
Bang !
|
||||||
|
PositionalArg
|
||||||
|
Word test.txt`)
|
||||||
|
|
||||||
|
expect('read-file!').toMatchTree(`
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier read-file
|
||||||
|
Bang !`)
|
||||||
|
|
||||||
|
expect('parse-json!').toMatchTree(`
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier parse-json
|
||||||
|
Bang !`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bang operator does not make identifier assignable', () => {
|
||||||
|
// thing! = true should fail to parse because thing! is a FunctionCallOrIdentifier, not AssignableIdentifier
|
||||||
|
expect('thing! = true').not.toMatchTree(`
|
||||||
|
Assign
|
||||||
|
AssignableIdentifier thing
|
||||||
|
Eq =
|
||||||
|
Boolean true`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('regular identifiers without bang can still be assigned', () => {
|
||||||
|
expect('thing = true').toMatchTree(`
|
||||||
|
Assign
|
||||||
|
AssignableIdentifier thing
|
||||||
|
Eq =
|
||||||
|
Boolean true`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bang works with multi-word identifiers', () => {
|
||||||
|
expect('read-my-file!').toMatchTree(`
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier read-my-file
|
||||||
|
Bang !`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bang works with emoji identifiers', () => {
|
||||||
|
expect('🚀!').toMatchTree(`
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier 🚀
|
||||||
|
Bang !`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bang in function call with multiple arguments', () => {
|
||||||
|
expect('fetch! url timeout').toMatchTree(`
|
||||||
|
FunctionCall
|
||||||
|
Identifier fetch
|
||||||
|
Bang !
|
||||||
|
PositionalArg
|
||||||
|
Identifier url
|
||||||
|
PositionalArg
|
||||||
|
Identifier timeout`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bang is context-sensitive: only an operator at end of identifier', () => {
|
||||||
|
// Bang followed by separator = operator
|
||||||
|
expect('read-file! test.txt').toMatchTree(`
|
||||||
|
FunctionCall
|
||||||
|
Identifier read-file
|
||||||
|
Bang !
|
||||||
|
PositionalArg
|
||||||
|
Word test.txt`)
|
||||||
|
|
||||||
|
expect('foo! (bar)').toMatchTree(`
|
||||||
|
FunctionCall
|
||||||
|
Identifier foo
|
||||||
|
Bang !
|
||||||
|
PositionalArg
|
||||||
|
ParenExpr
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier bar`)
|
||||||
|
|
||||||
|
// Bang in middle of word = part of Word token
|
||||||
|
expect('hi!mom').toMatchTree(`Word hi!mom`)
|
||||||
|
expect('hello!world!').toMatchTree(`Word hello!world!`)
|
||||||
|
expect('url://example.com!').toMatchTree(`Word url://example.com!`)
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Unicode Symbol Support', () => {
|
describe('Unicode Symbol Support', () => {
|
||||||
|
|
@ -324,7 +409,7 @@ describe('Parentheses', () => {
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('a word start with an operator', () => {
|
test.skip('a word start with an operator', () => {
|
||||||
const operators = ['*', '/', '+', '-', 'and', 'or', '=', '!=', '>=', '<=', '>', '<']
|
const operators = ['*', '/', '+', '-', 'and', 'or', '=', '!=', '>=', '<=', '>', '<']
|
||||||
for (const operator of operators) {
|
for (const operator of operators) {
|
||||||
expect(`find ${operator}cool*`).toMatchTree(`
|
expect(`find ${operator}cool*`).toMatchTree(`
|
||||||
|
|
@ -630,6 +715,20 @@ describe('Array destructuring', () => {
|
||||||
Number 1
|
Number 1
|
||||||
Number 2`)
|
Number 2`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('parses array destructuring with bang operator', () => {
|
||||||
|
expect('[ error content ] = read-file! test.txt').toMatchTree(`
|
||||||
|
Assign
|
||||||
|
Array
|
||||||
|
Identifier error
|
||||||
|
Identifier content
|
||||||
|
Eq =
|
||||||
|
FunctionCall
|
||||||
|
Identifier read-file
|
||||||
|
Bang !
|
||||||
|
PositionalArg
|
||||||
|
Word test.txt`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Conditional ops', () => {
|
describe('Conditional ops', () => {
|
||||||
|
|
|
||||||
278
src/parser/tests/exceptions.test.ts
Normal file
278
src/parser/tests/exceptions.test.ts
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
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
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -24,13 +24,13 @@ export const tokenizer = new ExternalTokenizer(
|
||||||
if (isDigit(ch)) return
|
if (isDigit(ch)) return
|
||||||
|
|
||||||
// Don't consume things that start with - or + followed by a digit (negative/positive numbers)
|
// Don't consume things that start with - or + followed by a digit (negative/positive numbers)
|
||||||
if ((ch === 45 /* - */ || ch === 43) /* + */ && isDigit(input.peek(1))) return
|
if ((ch === 45 /* - */ || ch === 43 /* + */) && isDigit(input.peek(1))) return
|
||||||
|
|
||||||
const isValidStart = isLowercaseLetter(ch) || isEmojiOrUnicode(ch)
|
const isValidStart = isLowercaseLetter(ch) || isEmojiOrUnicode(ch)
|
||||||
const canBeWord = stack.canShift(Word)
|
const canBeWord = stack.canShift(Word)
|
||||||
|
|
||||||
// Consume all word characters, tracking if it remains a valid identifier
|
// Consume all word characters, tracking if it remains a valid identifier
|
||||||
const { pos, isValidIdentifier, stoppedAtDot } = consumeWordToken(
|
const { pos, isValidIdentifier, stoppedAtDot, stoppedAtBang } = consumeWordToken(
|
||||||
input,
|
input,
|
||||||
isValidStart,
|
isValidStart,
|
||||||
canBeWord
|
canBeWord
|
||||||
|
|
@ -53,6 +53,30 @@ export const tokenizer = new ExternalTokenizer(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we should emit Identifier before Bang operator
|
||||||
|
if (stoppedAtBang) {
|
||||||
|
const nextCh = getFullCodePoint(input, pos + 1)
|
||||||
|
const isSeparator =
|
||||||
|
isWhiteSpace(nextCh) ||
|
||||||
|
nextCh === -1 /* EOF */ ||
|
||||||
|
nextCh === 10 /* \n */ ||
|
||||||
|
nextCh === 40 /* ( */ ||
|
||||||
|
nextCh === 91 /* [ */
|
||||||
|
|
||||||
|
if (isSeparator) {
|
||||||
|
input.advance(pos)
|
||||||
|
const token = chooseIdentifierToken(input, stack)
|
||||||
|
input.acceptToken(token)
|
||||||
|
} else {
|
||||||
|
// Continue consuming - the bang is part of a longer word
|
||||||
|
const afterBang = consumeRestOfWord(input, pos + 1, canBeWord)
|
||||||
|
input.advance(afterBang)
|
||||||
|
input.acceptToken(Word)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Advance past the token we consumed
|
// Advance past the token we consumed
|
||||||
input.advance(pos)
|
input.advance(pos)
|
||||||
|
|
||||||
|
|
@ -89,15 +113,16 @@ const buildIdentifierText = (input: InputStream, length: number): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume word characters, tracking if it remains a valid identifier
|
// Consume word characters, tracking if it remains a valid identifier
|
||||||
// Returns the position after consuming, whether it's a valid identifier, and if we stopped at a dot
|
// Returns the position after consuming, whether it's a valid identifier, and if we stopped at a dot or bang
|
||||||
const consumeWordToken = (
|
const consumeWordToken = (
|
||||||
input: InputStream,
|
input: InputStream,
|
||||||
isValidStart: boolean,
|
isValidStart: boolean,
|
||||||
canBeWord: boolean
|
canBeWord: boolean
|
||||||
): { pos: number; isValidIdentifier: boolean; stoppedAtDot: boolean } => {
|
): { pos: number; isValidIdentifier: boolean; stoppedAtDot: boolean; stoppedAtBang: boolean } => {
|
||||||
let pos = getCharSize(getFullCodePoint(input, 0))
|
let pos = getCharSize(getFullCodePoint(input, 0))
|
||||||
let isValidIdentifier = isValidStart
|
let isValidIdentifier = isValidStart
|
||||||
let stoppedAtDot = false
|
let stoppedAtDot = false
|
||||||
|
let stoppedAtBang = false
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const ch = getFullCodePoint(input, pos)
|
const ch = getFullCodePoint(input, pos)
|
||||||
|
|
@ -108,6 +133,12 @@ const consumeWordToken = (
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop at bang if we have a valid identifier (might be bang operator)
|
||||||
|
if (ch === 33 /* ! */ && isValidIdentifier) {
|
||||||
|
stoppedAtBang = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// Stop if we hit a non-word character
|
// Stop if we hit a non-word character
|
||||||
if (!isWordChar(ch)) break
|
if (!isWordChar(ch)) break
|
||||||
|
|
||||||
|
|
@ -127,7 +158,7 @@ const consumeWordToken = (
|
||||||
pos += getCharSize(ch)
|
pos += getCharSize(ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { pos, isValidIdentifier, stoppedAtDot }
|
return { pos, isValidIdentifier, stoppedAtDot, stoppedAtBang }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume the rest of a word after we've decided not to treat a dot as DotGet
|
// Consume the rest of a word after we've decided not to treat a dot as DotGet
|
||||||
|
|
|
||||||
|
|
@ -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: return 0
|
default: throw new Error(`length: expected string, array, or dict, got ${value.type}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -65,7 +65,24 @@ export const globals = {
|
||||||
identity: (v: any) => v,
|
identity: (v: any) => v,
|
||||||
|
|
||||||
// collections
|
// collections
|
||||||
at: (collection: any, index: number | string) => collection[index],
|
at: (collection: any, index: number | string) => {
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,14 @@ 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) => list.slice(0, n),
|
take: (list: any[], n: number) => {
|
||||||
drop: (list: any[], n: number) => list.slice(n),
|
if (n < 0) throw new Error(`take: count must be non-negative, got ${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),
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,24 @@ 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[]) => Math.min(...nums),
|
min: (...nums: number[]) => {
|
||||||
max: (...nums: number[]) => Math.max(...nums),
|
if (nums.length === 0) throw new Error('min: expected at least one argument')
|
||||||
|
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) => Math.sqrt(n),
|
sqrt: (n: number) => {
|
||||||
|
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) => Math.min(Math.max(n, min), max),
|
clamp: (n: number, min: number, max: number) => {
|
||||||
|
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,7 +21,11 @@ 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) => str.repeat(count),
|
repeat: (str: string, count: number) => {
|
||||||
|
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,9 +176,12 @@ 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)
|
|
||||||
await expect(`length null`).toEvaluateTo(0, globals)
|
test('length throws on invalid types', async () => {
|
||||||
|
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 () => {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ declare module 'bun:test' {
|
||||||
toMatchExpression(expected: string): T
|
toMatchExpression(expected: string): T
|
||||||
toFailParse(): T
|
toFailParse(): T
|
||||||
toEvaluateTo(expected: unknown, globals?: Record<string, any>): Promise<T>
|
toEvaluateTo(expected: unknown, globals?: Record<string, any>): Promise<T>
|
||||||
toFailEvaluation(): Promise<T>
|
toFailEvaluation(globals?: Record<string, any>): Promise<T>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,8 +99,7 @@ expect.extend({
|
||||||
assert(typeof received === 'string', 'toEvaluateTo can only be used with string values')
|
assert(typeof received === 'string', 'toEvaluateTo can only be used with string values')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (globals) setGlobals(Object.keys(globals))
|
const compiler = new Compiler(received, globals ? Object.keys(globals) : [])
|
||||||
const compiler = new Compiler(received)
|
|
||||||
const result = await run(compiler.bytecode, globals)
|
const result = await run(compiler.bytecode, globals)
|
||||||
let value = VMResultToValue(result)
|
let value = VMResultToValue(result)
|
||||||
|
|
||||||
|
|
@ -121,12 +120,12 @@ expect.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async toFailEvaluation(received) {
|
async toFailEvaluation(received, globals?: Record<string, any>) {
|
||||||
assert(typeof received === 'string', 'toFailEvaluation can only be used with string values')
|
assert(typeof received === 'string', 'toFailEvaluation can only be used with string values')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const compiler = new Compiler(received)
|
const compiler = new Compiler(received, globals ? Object.keys(globals) : [])
|
||||||
const vm = new VM(compiler.bytecode)
|
const vm = new VM(compiler.bytecode, globals)
|
||||||
const value = await vm.run()
|
const value = await vm.run()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user