restore errors, fancy printing

This commit is contained in:
Chris Wanstrath 2025-12-03 13:40:04 -08:00
parent 757a50e23e
commit 21e7ed41af
6 changed files with 84 additions and 36 deletions

View File

@ -66,12 +66,12 @@ export class Compiler {
if (globals) setGlobals(Array.isArray(globals) ? globals : Object.keys(globals)) if (globals) setGlobals(Array.isArray(globals) ? globals : Object.keys(globals))
const ast = parse(input) const ast = parse(input)
const cst = new Tree(ast) const cst = new Tree(ast)
// const errors = checkTreeForErrors(cst) const errors = checkTreeForErrors(cst)
// const firstError = errors[0] const firstError = errors[0]
// if (firstError) { if (firstError) {
// throw firstError throw firstError
// } }
this.#compileCst(cst, input) this.#compileCst(cst, input)
this.bytecode = toBytecode(this.instructions) this.bytecode = toBytecode(this.instructions)

View File

@ -5,13 +5,13 @@ import type { SyntaxNode, Tree } from '#parser/node'
export const checkTreeForErrors = (tree: Tree): CompilerError[] => { export const checkTreeForErrors = (tree: Tree): CompilerError[] => {
const errors: CompilerError[] = [] const errors: CompilerError[] = []
// tree.iterate({ tree.iterate({
// enter: (node) => { enter: (node) => {
// if (node.type.isError) { if (node.type.isError) {
// errors.push(new CompilerError(`Unexpected syntax.`, node.from, node.to)) errors.push(new CompilerError(`Unexpected syntax.`, node.from, node.to))
// } }
// }, },
// }) })
return errors return errors
} }
@ -58,8 +58,7 @@ export const getAssignmentParts = (node: SyntaxNode) => {
if (!left || left.type.id !== terms.AssignableIdentifier) { if (!left || left.type.id !== terms.AssignableIdentifier) {
throw new CompilerError( throw new CompilerError(
`Assign left child must be an AssignableIdentifier or Array, got ${ `Assign left child must be an AssignableIdentifier or Array, got ${left ? left.type.name : 'none'
left ? left.type.name : 'none'
}`, }`,
node.from, node.from,
node.to node.to
@ -75,8 +74,7 @@ export const getCompoundAssignmentParts = (node: SyntaxNode) => {
if (!left || left.type.id !== terms.AssignableIdentifier) { if (!left || left.type.id !== terms.AssignableIdentifier) {
throw new CompilerError( throw new CompilerError(
`CompoundAssign left child must be an AssignableIdentifier, got ${ `CompoundAssign left child must be an AssignableIdentifier, got ${left ? left.type.name : 'none'
left ? left.type.name : 'none'
}`, }`,
node.from, node.from,
node.to node.to

View File

@ -128,6 +128,15 @@ export class Tree {
node: this.topNode, node: this.topNode,
} }
} }
iterate(options: { enter: (node: SyntaxNode) => void }) {
const iter = (node: SyntaxNode) => {
for (const n of node.children) iter(n)
options.enter(node)
}
iter(this.topNode)
}
} }
// TODO: TEMPORARY SHIM // TODO: TEMPORARY SHIM
@ -295,7 +304,6 @@ class SyntaxNodeType {
case 'keyword': case 'keyword':
return term.keyword return term.keyword
} }
return 0 return 0
} }
@ -307,6 +315,7 @@ class SyntaxNodeType {
export class SyntaxNode { export class SyntaxNode {
#type: NodeType #type: NodeType
#isError = false
from: number from: number
to: number to: number
parent: SyntaxNode | null parent: SyntaxNode | null
@ -336,7 +345,11 @@ export class SyntaxNode {
} }
get isError(): boolean { get isError(): boolean {
return false return this.#isError
}
set isError(err: boolean) {
this.#isError = err
} }
get firstChild(): SyntaxNode | null { get firstChild(): SyntaxNode | null {

View File

@ -1,7 +1,9 @@
import { CompilerError } from '#compiler/compilerError'
import { Scanner, type Token, TokenType } from './tokenizer2' import { Scanner, type Token, TokenType } from './tokenizer2'
import { SyntaxNode, operators, precedence, conditionals, compounds } from './node' import { SyntaxNode, operators, precedence, conditionals, compounds } from './node'
import { globals } from './tokenizer' import { globals } from './tokenizer'
import { parseString } from './stringParser' import { parseString } from './stringParser'
import { Compiler } from '#compiler/compiler'
const $T = TokenType const $T = TokenType
@ -256,6 +258,7 @@ export class Parser {
return val return val
} else { } else {
const arg = new SyntaxNode('PositionalArg', val.from, val.to) const arg = new SyntaxNode('PositionalArg', val.from, val.to)
if (val.isError) arg.isError = true
arg.add(val) arg.add(val)
return arg return arg
} }
@ -356,7 +359,7 @@ export class Parser {
return SyntaxNode.from(this.next()) return SyntaxNode.from(this.next())
const next = this.next() const next = this.next()
throw `[atom] unexpected token ${TokenType[next.type]}: ${JSON.stringify(next)}\n\n ${this.input}\n` throw new CompilerError(`Unexpected token: ${TokenType[next.type]}`, next.from, next.to)
} }
// blocks in if, do, special calls, etc // blocks in if, do, special calls, etc
@ -432,6 +435,7 @@ export class Parser {
// [ a=1 b=true c='three' ] // [ a=1 b=true c='three' ]
dict(): SyntaxNode { dict(): SyntaxNode {
const open = this.expect($T.OpenBracket) const open = this.expect($T.OpenBracket)
let isError = false
// empty dict [=] or [ = ] // empty dict [=] or [ = ]
if (this.is($T.Operator, '=') && this.nextIs($T.CloseBracket)) { if (this.is($T.Operator, '=') && this.nextIs($T.CloseBracket)) {
@ -456,20 +460,29 @@ export class Parser {
if (this.nextIs($T.Operator, '=')) { if (this.nextIs($T.Operator, '=')) {
const ident = this.identifier() const ident = this.identifier()
const op = this.op('=') const op = this.op('=')
const val = this.arg(true)
const prefix = new SyntaxNode('NamedArgPrefix', ident.from, op.to) const prefix = new SyntaxNode('NamedArgPrefix', ident.from, op.to)
const node = new SyntaxNode('NamedArg', ident.from, val.to)
node.add(prefix) if (this.is($T.CloseBracket) || this.is($T.Semicolon) || this.is($T.Newline)) {
node.add(val) const node = new SyntaxNode('NamedArg', ident.from, op.to)
values.push(node) node.isError = true
isError = true
values.push(node.push(prefix))
} else { } else {
values.push(this.is($T.NamedArgPrefix) ? this.namedArg() : this.arg()) const val = this.arg(true)
const node = new SyntaxNode('NamedArg', ident.from, val.to)
values.push(node.push(prefix, val))
}
} else {
const arg = this.is($T.NamedArgPrefix) ? this.namedArg() : this.arg()
if (arg.isError) isError = true
values.push(arg)
} }
} }
const close = this.expect($T.CloseBracket) const close = this.expect($T.CloseBracket)
const node = new SyntaxNode('Dict', open.from, close.to) const node = new SyntaxNode('Dict', open.from, close.to)
node.isError = isError
return node.push(...values) return node.push(...values)
} }
@ -491,7 +504,7 @@ export class Parser {
else if (this.is($T.NamedArgPrefix)) else if (this.is($T.NamedArgPrefix))
arg = this.namedParam() arg = this.namedParam()
else else
throw `[do] expected Identifier or NamedArgPrefix, got ${JSON.stringify(this.current())}\n\n ${this.input}\n` throw new CompilerError(`Expected Identifier or NamedArgPrefix, got ${TokenType[this.current().type]}`, this.current().from, this.current().to)
params.push(arg) params.push(arg)
} }
@ -605,14 +618,20 @@ export class Parser {
// you're lookin at it // you're lookin at it
functionCall(fn?: SyntaxNode): SyntaxNode { functionCall(fn?: SyntaxNode): SyntaxNode {
const ident = fn ?? this.identifier() const ident = fn ?? this.identifier()
let isError = false
const args: SyntaxNode[] = [] const args: SyntaxNode[] = []
while (!this.isExprEnd()) while (!this.isExprEnd()) {
args.push(this.is($T.NamedArgPrefix) ? this.namedArg() : this.arg()) const arg = this.is($T.NamedArgPrefix) ? this.namedArg() : this.arg()
if (arg.isError) isError = true
args.push(arg)
}
const node = new SyntaxNode('FunctionCall', ident.from, (args.at(-1) || ident).to) const node = new SyntaxNode('FunctionCall', ident.from, (args.at(-1) || ident).to)
node.push(ident, ...args) node.push(ident, ...args)
if (isError) node.isError = true
if (!this.inTestExpr && this.is($T.Colon)) { if (!this.inTestExpr && this.is($T.Colon)) {
const block = this.block() const block = this.block()
const end = this.keyword('end') const end = this.keyword('end')
@ -718,6 +737,13 @@ export class Parser {
// abc= true // abc= true
namedArg(): SyntaxNode { namedArg(): SyntaxNode {
const prefix = SyntaxNode.from(this.expect($T.NamedArgPrefix)) const prefix = SyntaxNode.from(this.expect($T.NamedArgPrefix))
if (this.isExprEnd()) {
const node = new SyntaxNode('NamedArg', prefix.from, prefix.to)
node.isError = true
return node.push(prefix)
}
const val = this.arg(true) const val = this.arg(true)
const node = new SyntaxNode('NamedArg', prefix.from, val.to) const node = new SyntaxNode('NamedArg', prefix.from, val.to)
return node.push(prefix, val) return node.push(prefix, val)
@ -729,7 +755,7 @@ export class Parser {
const val = this.value() const val = this.value()
if (!['Null', 'Boolean', 'Number', 'String'].includes(val.type.name)) if (!['Null', 'Boolean', 'Number', 'String'].includes(val.type.name))
throw `[namedParam] default value must be Null|Bool|Num|Str, got ${val.type}\n\n ${this.input}\n` throw new CompilerError(`Default value must be null, boolean, number, or string, got ${val.type.name}`, val.from, val.to)
const node = new SyntaxNode('NamedParam', prefix.from, val.to) const node = new SyntaxNode('NamedParam', prefix.from, val.to)
return node.push(prefix, val) return node.push(prefix, val)
@ -739,7 +765,7 @@ export class Parser {
op(op?: string): SyntaxNode { op(op?: string): SyntaxNode {
const token = op ? this.expect($T.Operator, op) : this.expect($T.Operator) const token = op ? this.expect($T.Operator, op) : this.expect($T.Operator)
const name = operators[token.value!] const name = operators[token.value!]
if (!name) throw `[op] operator not registered: ${token.value!}\n\n ${this.input}\n` if (!name) throw new CompilerError(`Operator not registered: ${token.value!}`, token.from, token.to)
return new SyntaxNode(name, token.from, token.to) return new SyntaxNode(name, token.from, token.to)
} }
@ -919,7 +945,7 @@ export class Parser {
expect(type: TokenType, value?: string): Token | never { expect(type: TokenType, value?: string): Token | never {
if (!this.is(type, value)) { if (!this.is(type, value)) {
const token = this.current() const token = this.current()
throw `expected ${TokenType[type]}${value ? ` "${value}"` : ''}, got ${TokenType[token?.type || 0]}${token?.value ? ` "${token.value}"` : ''} at position ${this.pos}\n\n ${this.input}\n` throw new CompilerError(`Expected ${TokenType[type]}${value ? ` "${value}"` : ''}, got ${TokenType[token?.type || 0]}${token?.value ? ` "${token.value}"` : ''} at position ${this.pos}`, token.from, token.to)
} }
return this.next() return this.next()
} }

View File

@ -109,7 +109,7 @@ describe('calling functions', () => {
`) `)
}) })
test.skip('Incomplete namedArg', () => { test('Incomplete namedArg', () => {
expect('tail lines=').toMatchTree(` expect('tail lines=').toMatchTree(`
FunctionCall FunctionCall
Identifier tail Identifier tail

View File

@ -24,10 +24,21 @@ export const treeToString2 = (tree: SyntaxNode, input: string, depth = 0): strin
if (node.name === 'Program') node = node.firstChild if (node.name === 'Program') node = node.firstChild
while (node) { while (node) {
// If this node is an error, print ⚠ instead of its content
if (node.isError && !node.firstChild) {
lines.push(' '.repeat(depth) + '⚠')
} else {
lines.push(nodeToString(node, input, depth)) lines.push(nodeToString(node, input, depth))
if (node.firstChild) if (node.firstChild) {
lines.push(treeToString2(node.firstChild, input, depth + 1)) lines.push(treeToString2(node.firstChild, input, depth + 1))
}
// If this node has an error, add ⚠ after its children
if (node.isError && node.firstChild) {
lines.push(' '.repeat(depth === 0 ? 0 : depth + 1) + '⚠')
}
}
node = node.nextSibling node = node.nextSibling
} }