shrimp/src/compiler/compiler.ts

929 lines
30 KiB
TypeScript

import { CompilerError } from '#compiler/compilerError.ts'
import { parser } from '#parser/shrimp.ts'
import * as terms from '#parser/shrimp.terms'
import { setGlobals } from '#parser/tokenizer'
import { tokenizeCurlyString } from '#parser/curlyTokenizer'
import type { SyntaxNode, Tree } from '@lezer/common'
import { assert, errorMessage } from '#utils/utils'
import { toBytecode, type Bytecode, type ProgramItem, bytecodeToString } from 'reefvm'
import {
checkTreeForErrors,
getAllChildren,
getAssignmentParts,
getCompoundAssignmentParts,
getBinaryParts,
getDotGetParts,
getFunctionCallParts,
getFunctionDefParts,
getIfExprParts,
getNamedArgParts,
getPipeExprParts,
getStringParts,
getTryExprParts,
} from '#compiler/utils'
const DEBUG = false
// const DEBUG = true
type Label = `.${string}`
// Process escape sequences in strings
function processEscapeSeq(escapeSeq: string): string {
// escapeSeq includes the backslash, e.g., "\n", "\$", "\\"
if (escapeSeq.length !== 2) return escapeSeq
switch (escapeSeq[1]) {
case 'n':
return '\n'
case 't':
return '\t'
case 'r':
return '\r'
case '\\':
return '\\'
case "'":
return "'"
case '$':
return '$'
default:
return escapeSeq // Unknown escape, keep as-is
}
}
export class Compiler {
instructions: ProgramItem[] = []
labelCount = 0
fnLabelCount = 0
ifLabelCount = 0
tryLabelCount = 0
loopLabelCount = 0
bytecode: Bytecode
pipeCounter = 0
constructor(public input: string, globals?: string[] | Record<string, any>) {
try {
if (globals) setGlobals(Array.isArray(globals) ? globals : Object.keys(globals))
const cst = parser.parse(input)
const errors = checkTreeForErrors(cst)
const firstError = errors[0]
if (firstError) {
throw firstError
}
this.#compileCst(cst, input)
this.bytecode = toBytecode(this.instructions)
if (DEBUG) {
const bytecodeString = bytecodeToString(this.bytecode)
console.log(`\n🤖 bytecode:\n----------------\n${bytecodeString}\n\n`)
console.log(`\n🤖 bytecode:\n----------------\n${this.instructions}\n\n`)
}
} catch (error) {
if (error instanceof CompilerError) {
throw new Error(error.toReadableString(input))
} else {
throw new Error(`Unknown error during compilation:\n${errorMessage(error)}`)
}
}
}
#compileCst(cst: Tree, input: string) {
const isProgram = cst.topNode.type.id === terms.Program
assert(isProgram, `Expected Program node, got ${cst.topNode.type.name}`)
let child = cst.topNode.firstChild
while (child) {
this.instructions.push(...this.#compileNode(child, input))
child = child.nextSibling
}
this.instructions.push(['HALT'])
}
#compileNode(node: SyntaxNode, input: string): ProgramItem[] {
const value = input.slice(node.from, node.to)
if (DEBUG) console.log(`🫦 ${node.name}: ${value}`)
switch (node.type.id) {
case terms.Number:
// Handle sign prefix for hex, binary, and octal literals
// Number() doesn't parse '-0xFF', '+0xFF', '-0o77', etc. correctly
let numberValue: number
if (value.startsWith('-') && (value.includes('0x') || value.includes('0b') || value.includes('0o'))) {
numberValue = -Number(value.slice(1))
} else if (value.startsWith('+') && (value.includes('0x') || value.includes('0b') || value.includes('0o'))) {
numberValue = Number(value.slice(1))
} else {
numberValue = Number(value)
}
if (Number.isNaN(numberValue))
throw new CompilerError(`Invalid number literal: ${value}`, node.from, node.to)
return [[`PUSH`, numberValue]]
case terms.String: {
if (node.firstChild?.type.id === terms.CurlyString)
return this.#compileCurlyString(value, input)
const { parts, hasInterpolation } = getStringParts(node, input)
// Simple string without interpolation or escapes - extract text directly
if (!hasInterpolation) {
// Remove surrounding quotes and return as-is
const strValue = value.slice(1, -1)
return [['PUSH', strValue]]
}
// String with interpolation or escapes - compile each part and concatenate
const instructions: ProgramItem[] = []
parts.forEach((part) => {
const partValue = input.slice(part.from, part.to)
switch (part.type.id) {
case terms.StringFragment:
// Plain text fragment - just push as-is
instructions.push(['PUSH', partValue])
break
case terms.EscapeSeq:
// Process escape sequence and push the result
const processed = processEscapeSeq(partValue)
instructions.push(['PUSH', processed])
break
case terms.Interpolation:
// Interpolation contains either Identifier or ParenExpr (the $ is anonymous)
const child = part.firstChild
if (!child) {
throw new CompilerError('Interpolation has no child', part.from, part.to)
}
// Compile the Identifier or ParenExpr
instructions.push(...this.#compileNode(child, input))
break
default:
throw new CompilerError(
`Unexpected string part: ${part.type.name}`,
part.from,
part.to
)
}
})
// Use STR_CONCAT to join all parts
instructions.push(['STR_CONCAT', parts.length])
return instructions
}
case terms.Boolean: {
return [[`PUSH`, value === 'true']]
}
case terms.Null: {
return [[`PUSH`, null]]
}
case terms.Regex: {
// remove the surrounding slashes and any flags
const [_, pattern, flags] = value.match(/^\/\/(.*)\/\/([gimsuy]*)$/) || []
if (!pattern) {
throw new CompilerError(`Invalid regex literal: ${value}`, node.from, node.to)
}
let regex: RegExp
try {
regex = new RegExp(pattern, flags)
} catch (e) {
throw new CompilerError(`Invalid regex literal: ${value}`, node.from, node.to)
}
return [['PUSH', regex]]
}
case terms.Identifier: {
return [[`TRY_LOAD`, value]]
}
case terms.Word: {
return [['PUSH', value]]
}
case terms.DotGet: {
// DotGet is parsed into a nested tree because it's hard to parse it into a flat one.
// However, we want a flat tree - so we're going to pretend like we are getting one from the parser.
//
// This: DotGet(config, DotGet(script, name))
// Becomes: DotGet(config, script, name)
const { objectName, property } = getDotGetParts(node, input)
const instructions: ProgramItem[] = []
instructions.push(['TRY_LOAD', objectName])
const flattenProperty = (prop: SyntaxNode): void => {
if (prop.type.id === terms.DotGet) {
const nestedParts = getDotGetParts(prop, input)
const nestedObjectValue = input.slice(nestedParts.object.from, nestedParts.object.to)
instructions.push(['PUSH', nestedObjectValue])
instructions.push(['DOT_GET'])
flattenProperty(nestedParts.property)
} else {
if (prop.type.id === terms.ParenExpr) {
instructions.push(...this.#compileNode(prop, input))
} else {
const propertyValue = input.slice(prop.from, prop.to)
instructions.push(['PUSH', propertyValue])
}
instructions.push(['DOT_GET'])
}
}
flattenProperty(property)
return instructions
}
case terms.BinOp: {
const { left, op, right } = getBinaryParts(node)
const instructions: ProgramItem[] = []
instructions.push(...this.#compileNode(left, input))
instructions.push(...this.#compileNode(right, input))
const opValue = input.slice(op.from, op.to)
switch (opValue) {
case '+':
instructions.push(['ADD'])
break
case '-':
instructions.push(['SUB'])
break
case '*':
instructions.push(['MUL'])
break
case '/':
instructions.push(['DIV'])
break
case '%':
instructions.push(['MOD'])
break
case 'band':
instructions.push(['BIT_AND'])
break
case 'bor':
instructions.push(['BIT_OR'])
break
case 'bxor':
instructions.push(['BIT_XOR'])
break
case '<<':
instructions.push(['BIT_SHL'])
break
case '>>':
instructions.push(['BIT_SHR'])
break
case '>>>':
instructions.push(['BIT_USHR'])
break
default:
throw new CompilerError(`Unsupported binary operator: ${opValue}`, op.from, op.to)
}
return instructions
}
case terms.Assign: {
const assignParts = getAssignmentParts(node)
const instructions: ProgramItem[] = []
// right-hand side
instructions.push(...this.#compileNode(assignParts.right, input))
// array destructuring: [ a b ] = [ 1 2 3 4 ]
if ('arrayPattern' in assignParts) {
const identifiers = assignParts.arrayPattern ?? []
if (identifiers.length === 0) return instructions
for (let i = 0; i < identifiers.length; i++) {
instructions.push(['DUP'])
instructions.push(['PUSH', i])
instructions.push(['DOT_GET'])
instructions.push(['STORE', input.slice(identifiers[i]!.from, identifiers[i]!.to)])
}
// original array still on stack as the return value
return instructions
}
// simple assignment: x = value
instructions.push(['DUP'])
const identifierName = input.slice(assignParts.identifier.from, assignParts.identifier.to)
instructions.push(['STORE', identifierName])
return instructions
}
case terms.CompoundAssign: {
const { identifier, operator, right } = getCompoundAssignmentParts(node)
const identifierName = input.slice(identifier.from, identifier.to)
const instructions: ProgramItem[] = []
const opValue = input.slice(operator.from, operator.to)
// Special handling for ??= since it needs conditional evaluation
if (opValue === '??=') {
instructions.push(['LOAD', identifierName])
const rightInstructions = this.#compileNode(right, input)
instructions.push(['DUP'])
instructions.push(['PUSH', null])
instructions.push(['NEQ'])
instructions.push(['JUMP_IF_TRUE', rightInstructions.length + 1])
instructions.push(['POP'])
instructions.push(...rightInstructions)
instructions.push(['DUP'])
instructions.push(['STORE', identifierName])
return instructions
}
// Standard compound assignments: evaluate both sides, then operate
instructions.push(['LOAD', identifierName]) // will throw if undefined
instructions.push(...this.#compileNode(right, input))
switch (opValue) {
case '+=':
instructions.push(['ADD'])
break
case '-=':
instructions.push(['SUB'])
break
case '*=':
instructions.push(['MUL'])
break
case '/=':
instructions.push(['DIV'])
break
case '%=':
instructions.push(['MOD'])
break
default:
throw new CompilerError(
`Unknown compound operator: ${opValue}`,
operator.from,
operator.to
)
}
// DUP and store (same as regular assignment)
instructions.push(['DUP'])
instructions.push(['STORE', identifierName])
return instructions
}
case terms.ParenExpr: {
const child = node.firstChild
if (!child) return [] // I guess it is empty parentheses?
return this.#compileNode(child, input)
}
case terms.FunctionDef: {
const { paramNames, bodyNodes, catchVariable, catchBody, finallyBody } =
getFunctionDefParts(node, input)
const instructions: ProgramItem[] = []
const functionLabel: Label = `.func_${this.fnLabelCount++}`
const afterLabel: Label = `.after_${functionLabel}`
instructions.push(['JUMP', afterLabel])
instructions.push([`${functionLabel}:`])
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([`${afterLabel}:`])
instructions.push(['MAKE_FUNCTION', paramNames, functionLabel])
return instructions
}
case terms.FunctionCallOrIdentifier: {
if (node.firstChild?.type.id === terms.DotGet) {
const instructions: ProgramItem[] = []
const callLabel = `.call_dotget_${++this.labelCount}`
const afterLabel = `.after_dotget_${++this.labelCount}`
instructions.push(...this.#compileNode(node.firstChild, input))
instructions.push(['DUP'])
instructions.push(['TYPE'])
instructions.push(['PUSH', 'function'])
instructions.push(['EQ'])
instructions.push(['JUMP_IF_TRUE', callLabel])
instructions.push(['DUP'])
instructions.push(['TYPE'])
instructions.push(['PUSH', 'native'])
instructions.push(['EQ'])
instructions.push(['JUMP_IF_TRUE', callLabel])
instructions.push(['JUMP', afterLabel])
instructions.push([`${callLabel}:`])
instructions.push(['PUSH', 0])
instructions.push(['PUSH', 0])
instructions.push(['CALL'])
instructions.push([`${afterLabel}:`])
return instructions
}
return [['TRY_CALL', value]]
}
/*
### Function Calls
Stack order (bottom to top):
LOAD fn
PUSH arg1 ; Positional args
PUSH arg2
PUSH "name" ; Named arg key
PUSH "value" ; Named arg value
PUSH 2 ; Positional count
PUSH 1 ; Named count
CALL
*/
case terms.FunctionCall: {
const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts(node, input)
const instructions: ProgramItem[] = []
instructions.push(...this.#compileNode(identifierNode, input))
positionalArgs.forEach((arg) => {
instructions.push(...this.#compileNode(arg, input))
})
namedArgs.forEach((arg) => {
const { name, valueNode } = getNamedArgParts(arg, input)
instructions.push(['PUSH', name])
instructions.push(...this.#compileNode(valueNode, input))
})
instructions.push(['PUSH', positionalArgs.length])
instructions.push(['PUSH', namedArgs.length])
instructions.push(['CALL'])
return instructions
}
case terms.Block: {
const children = getAllChildren(node)
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.FunctionCallWithBlock: {
const [fn, _colon, ...block] = getAllChildren(node)
let instructions: ProgramItem[] = []
const fnLabel: Label = `.func_${this.fnLabelCount++}`
const afterLabel: Label = `.after_${fnLabel}`
instructions.push(['JUMP', afterLabel])
instructions.push([`${fnLabel}:`])
instructions.push(
...block
.filter((x) => x.type.name !== 'keyword')
.map((x) => this.#compileNode(x!, input))
.flat()
)
instructions.push(['RETURN'])
instructions.push([`${afterLabel}:`])
if (fn?.type.id === terms.FunctionCallOrIdentifier) {
instructions.push(['LOAD', input.slice(fn!.from, fn!.to)])
instructions.push(['MAKE_FUNCTION', [], fnLabel])
instructions.push(['PUSH', 1])
instructions.push(['PUSH', 0])
instructions.push(['CALL'])
} else if (fn?.type.id === terms.FunctionCall) {
let body = this.#compileNode(fn!, input)
const namedArgCount = (body[body.length - 2]![1] as number) * 2
const startSlice = body.length - namedArgCount - 3
body = [
...body.slice(0, startSlice),
['MAKE_FUNCTION', [], fnLabel],
...body.slice(startSlice),
]
// @ts-ignore
body[body.length - 3]![1] += 1
instructions.push(...body)
} else {
throw new Error(
`FunctionCallWithBlock: Expected FunctionCallOrIdentifier or FunctionCall`
)
}
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
}
case terms.IfExpr: {
const { conditionNode, thenBlock, elseIfBlocks, elseThenBlock } = getIfExprParts(
node,
input
)
const instructions: ProgramItem[] = []
instructions.push(...this.#compileNode(conditionNode, input))
this.ifLabelCount++
const endLabel: Label = `.end_${this.ifLabelCount}`
const thenBlockInstructions = this.#compileNode(thenBlock, input)
instructions.push(['JUMP_IF_FALSE', thenBlockInstructions.length + 1])
instructions.push(...thenBlockInstructions)
instructions.push(['JUMP', endLabel])
// Else if
elseIfBlocks.forEach(({ conditional, thenBlock }) => {
instructions.push(...this.#compileNode(conditional, input))
const elseIfInstructions = this.#compileNode(thenBlock, input)
instructions.push(['JUMP_IF_FALSE', elseIfInstructions.length + 1])
instructions.push(...elseIfInstructions)
instructions.push(['JUMP', endLabel])
})
// Else
if (elseThenBlock) {
const elseThenInstructions = this.#compileNode(elseThenBlock, input)
instructions.push(...elseThenInstructions)
} else {
instructions.push(['PUSH', null])
}
instructions.push([`${endLabel}:`])
return instructions
}
// - `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean
case terms.ConditionalOp: {
const instructions: ProgramItem[] = []
const { left, op, right } = getBinaryParts(node)
const leftInstructions: ProgramItem[] = this.#compileNode(left, input)
const rightInstructions: ProgramItem[] = this.#compileNode(right, input)
const opValue = input.slice(op.from, op.to)
switch (opValue) {
case '==':
instructions.push(...leftInstructions, ...rightInstructions, ['EQ'])
break
case '!=':
instructions.push(...leftInstructions, ...rightInstructions, ['NEQ'])
break
case '<':
instructions.push(...leftInstructions, ...rightInstructions, ['LT'])
break
case '>':
instructions.push(...leftInstructions, ...rightInstructions, ['GT'])
break
case '<=':
instructions.push(...leftInstructions, ...rightInstructions, ['LTE'])
break
case '>=':
instructions.push(...leftInstructions, ...rightInstructions, ['GTE'])
break
case 'and':
instructions.push(...leftInstructions)
instructions.push(['DUP'])
instructions.push(['JUMP_IF_FALSE', rightInstructions.length + 1])
instructions.push(['POP'])
instructions.push(...rightInstructions)
break
case 'or':
instructions.push(...leftInstructions)
instructions.push(['DUP'])
instructions.push(['JUMP_IF_TRUE', rightInstructions.length + 1])
instructions.push(['POP'])
instructions.push(...rightInstructions)
break
case '??':
// Nullish coalescing: return left if not null, else right
instructions.push(...leftInstructions)
instructions.push(['DUP'])
instructions.push(['PUSH', null])
instructions.push(['NEQ'])
instructions.push(['JUMP_IF_TRUE', rightInstructions.length + 1])
instructions.push(['POP'])
instructions.push(...rightInstructions)
break
default:
throw new CompilerError(`Unsupported conditional operator: ${opValue}`, op.from, op.to)
}
return instructions
}
case terms.PipeExpr: {
const { pipedFunctionCall, pipeReceivers } = getPipeExprParts(node)
if (!pipedFunctionCall || pipeReceivers.length === 0) {
throw new CompilerError('PipeExpr must have at least two operands', node.from, node.to)
}
const instructions: ProgramItem[] = []
instructions.push(...this.#compileNode(pipedFunctionCall, input))
this.pipeCounter++
const pipeValName = `_pipe_value_${this.pipeCounter}`
pipeReceivers.forEach((pipeReceiver) => {
instructions.push(['STORE', pipeValName])
const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts(
pipeReceiver,
input
)
instructions.push(...this.#compileNode(identifierNode, input))
const isUnderscoreInPositionalArgs = positionalArgs.some(
(arg) => arg.type.id === terms.Underscore
)
const isUnderscoreInNamedArgs = namedArgs.some((arg) => {
const { valueNode } = getNamedArgParts(arg, input)
return valueNode.type.id === terms.Underscore
})
const shouldPushPositionalArg = !isUnderscoreInPositionalArgs && !isUnderscoreInNamedArgs
// If no underscore is explicitly used, add the piped value as the first positional arg
if (shouldPushPositionalArg) {
instructions.push(['LOAD', pipeValName])
}
positionalArgs.forEach((arg) => {
if (arg.type.id === terms.Underscore) {
instructions.push(['LOAD', pipeValName])
} else {
instructions.push(...this.#compileNode(arg, input))
}
})
namedArgs.forEach((arg) => {
const { name, valueNode } = getNamedArgParts(arg, input)
instructions.push(['PUSH', name])
if (valueNode.type.id === terms.Underscore) {
instructions.push(['LOAD', pipeValName])
} else {
instructions.push(...this.#compileNode(valueNode, input))
}
})
instructions.push(['PUSH', positionalArgs.length + (shouldPushPositionalArg ? 1 : 0)])
instructions.push(['PUSH', namedArgs.length])
instructions.push(['CALL'])
})
return instructions
}
case terms.Array: {
const children = getAllChildren(node)
// We can easily parse [=] as an empty dict, but `[ = ]` is tougher.
// = can be a valid word, and is also valid inside words, so for now we cheat
// and check for arrays that look like `[ = ]` to interpret them as
// empty dicts
if (children.length === 1 && children[0]!.type.id === terms.Word) {
const child = children[0]!
if (input.slice(child.from, child.to) === '=') {
return [['MAKE_DICT', 0]]
}
}
const instructions: ProgramItem[] = children.map((x) => this.#compileNode(x, input)).flat()
instructions.push(['MAKE_ARRAY', children.length])
return instructions
}
case terms.Dict: {
const children = getAllChildren(node)
const instructions: ProgramItem[] = []
children.forEach((node) => {
const keyNode = node.firstChild
const valueNode = node.firstChild!.nextSibling
// name= -> name
const key = input.slice(keyNode!.from, keyNode!.to).slice(0, -1)
instructions.push(['PUSH', key])
instructions.push(...this.#compileNode(valueNode!, input))
})
instructions.push(['MAKE_DICT', children.length])
return instructions
}
case terms.WhileExpr: {
const [_while, test, _colon, block] = getAllChildren(node)
const instructions: ProgramItem[] = []
this.loopLabelCount++
const startLoop = `.loop_${this.loopLabelCount}:`
const endLoop = `.end_loop_${this.loopLabelCount}:`
instructions.push([`${startLoop}:`])
instructions.push(...this.#compileNode(test!, input))
instructions.push(['JUMP_IF_FALSE', endLoop])
instructions.push(...this.#compileNode(block!, input))
instructions.push(['JUMP', startLoop])
instructions.push([`${endLoop}:`])
return instructions
}
case terms.Import: {
const instructions: ProgramItem[] = []
const [_import, ...nodes] = getAllChildren(node)
const args = nodes.filter(node => node.type.id === terms.Identifier)
const namedArgs = nodes.filter(node => node.type.id === terms.NamedArg)
instructions.push(['LOAD', 'import'])
args.forEach((dict) =>
instructions.push(['PUSH', input.slice(dict.from, dict.to)])
)
namedArgs.forEach((arg) => {
const { name, valueNode } = getNamedArgParts(arg, input)
instructions.push(['PUSH', name])
instructions.push(...this.#compileNode(valueNode, input))
})
instructions.push(['PUSH', args.length])
instructions.push(['PUSH', namedArgs.length])
instructions.push(['CALL'])
return instructions
}
case terms.Comment: {
return [] // ignore comments
}
default:
throw new CompilerError(
`Compiler doesn't know how to handle a "${node.type.name}" node.`,
node.from,
node.to
)
}
}
#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
}
#compileCurlyString(value: string, input: string): ProgramItem[] {
const instructions: ProgramItem[] = []
const nodes = tokenizeCurlyString(value)
nodes.forEach((node) => {
if (typeof node === 'string') {
instructions.push(['PUSH', node])
} else {
const [input, topNode] = node
let child = topNode.firstChild
while (child) {
instructions.push(...this.#compileNode(child, input))
child = child.nextSibling
}
}
})
instructions.push(['STR_CONCAT', nodes.length])
return instructions
}
}