diff --git a/src/evaluator/evaluator.ts b/src/evaluator/evaluator.ts index ac62ad5..b1b8664 100644 --- a/src/evaluator/evaluator.ts +++ b/src/evaluator/evaluator.ts @@ -1,6 +1,8 @@ import { Tree, type SyntaxNode } from '@lezer/common' import * as terms from '../parser/shrimp.terms.ts' import { RuntimeError } from '#evaluator/runtimeError.ts' +import { assert } from 'console' +import { assertNever } from '#utils/utils.tsx' export const evaluate = (input: string, tree: Tree, context: Context) => { let result = undefined @@ -22,8 +24,20 @@ export const evaluate = (input: string, tree: Tree, context: Context) => { } const evaluateNode = (node: SyntaxNode, input: string, context: Context): any => { - const evalNode = syntaxNodeToEvalNode(node, input, context) + try { + const evalNode = syntaxNodeToEvalNode(node, input, context) + return evaluateEvalNode(evalNode, input, context) + } catch (error) { + if (error instanceof RuntimeError) { + throw error + } else { + console.error(error) + throw new RuntimeError('Error evaluating node', node.from, node.to) + } + } +} +const evaluateEvalNode = (evalNode: EvalNode, input: string, context: Context): any => { switch (evalNode.kind) { case 'number': case 'string': @@ -35,21 +49,20 @@ const evaluateNode = (node: SyntaxNode, input: string, context: Context): any => if (context.has(name)) { return context.get(name) } else { - throw new RuntimeError(`Undefined variable "${name}"`, node.from, node.to) + throw new RuntimeError(`Undefined variable "${name}"`, evalNode.node.from, evalNode.node.to) } } case 'assignment': { const name = evalNode.name - const value = evaluateNode(evalNode.value.node, input, context) + const value = evaluateEvalNode(evalNode.value, input, context) context.set(name, value) - return value } case 'binop': { - const left = evaluateNode(evalNode.left, input, context) - const right = evaluateNode(evalNode.right, input, context) + const left = evaluateEvalNode(evalNode.left, input, context) + const right = evaluateEvalNode(evalNode.right, input, context) if (evalNode.op === '+') { return left + right @@ -60,9 +73,27 @@ const evaluateNode = (node: SyntaxNode, input: string, context: Context): any => } else if (evalNode.op === '/') { return left / right } else { - throw new RuntimeError(`Unsupported operator "${evalNode.op}"`, node.from, node.to) + throw new RuntimeError( + `Unsupported operator "${evalNode.op}"`, + evalNode.node.from, + evalNode.node.to + ) } } + + case 'arg': { + // Just evaluate the arg's value + return evaluateEvalNode(evalNode.value, input, context) + } + + case 'command': { + // TODO: Actually execute the command + // For now, just return undefined + return undefined + } + + default: + assertNever(evalNode) } } @@ -73,8 +104,9 @@ type EvalNode = | { kind: 'string'; value: string; node: SyntaxNode } | { kind: 'boolean'; value: boolean; node: SyntaxNode } | { kind: 'identifier'; name: string; node: SyntaxNode } - | { kind: 'binop'; op: Operators; left: SyntaxNode; right: SyntaxNode; node: SyntaxNode } + | { kind: 'binop'; op: Operators; left: EvalNode; right: EvalNode; node: SyntaxNode } | { kind: 'assignment'; name: string; value: EvalNode; node: SyntaxNode } + | { kind: 'arg'; name?: string; value: EvalNode; node: SyntaxNode } | { kind: 'command'; name: string; args: EvalNode[]; node: SyntaxNode } const syntaxNodeToEvalNode = (node: SyntaxNode, input: string, context: Context): EvalNode => { @@ -94,78 +126,122 @@ const syntaxNodeToEvalNode = (node: SyntaxNode, input: string, context: Context) return { kind: 'identifier', name: value, node } case terms.BinOp: { - const [left, op, right] = destructure(node, ['*', '*', '*']) + const { left, op, right } = getBinaryParts(node) const opString = input.slice(op.from, op.to) as Operators - return { kind: 'binop', op: opString, left, right, node } + const leftNode = syntaxNodeToEvalNode(left, input, context) + const rightNode = syntaxNodeToEvalNode(right, input, context) + return { kind: 'binop', op: opString, left: leftNode, right: rightNode, node } } case terms.Assignment: { - const [identifier, _equals, expr] = destructure(node, [terms.Identifier, '*', '*']) - + const { identifier, value: expr } = getAssignmentParts(node) const name = input.slice(identifier.from, identifier.to) const value = syntaxNodeToEvalNode(expr, input, context) - return { kind: 'assignment', name, value, node } } case terms.ParenExpr: { - const [_leftParen, expr, _rightParen] = destructure(node, ['*', '*', '*']) + const expr = getParenParts(node) return syntaxNodeToEvalNode(expr, input, context) } case terms.CommandCall: { - const [_at, identifier, _leftParen, ...rest] = destructure(node, [ - '*', - terms.Identifier, - '*', - '*', - ]) + const { commandName, argNodes } = extractCommand(node, input) + + const args = argNodes.map((argNode) => { + const children = getAllChildren(argNode) + + if (argNode.type.id === terms.Arg) { + const [child] = children + if (!child) { + throw new Error(`Parser bug: Arg node has ${children.length} children, expected 1`) + } + const value = syntaxNodeToEvalNode(child, input, context) + return { kind: 'arg', value, node: argNode } as const + } + + if (argNode.type.id === terms.NamedArg) { + const [nameChild, valueChild] = children + if (!nameChild || !valueChild) { + throw new Error(`Parser bug: NamedArg node has ${children.length} children, expected 2`) + } + const namePrefix = input.slice(nameChild.from, nameChild.to) + const name = namePrefix.slice(0, -1) // Remove '=' + const value = syntaxNodeToEvalNode(valueChild, input, context) + return { kind: 'arg', name, value, node: argNode } as const + } + + throw new Error(`Parser bug: Unexpected arg node type: ${argNode.type.name}`) + }) + + return { kind: 'command', name: commandName, args, node } + } } throw new RuntimeError(`Unsupported node type "${node.type.name}"`, node.from, node.to) } -/* -The code below is a... -SIN AGAINST GOD! -...but it makes it easier to use above -*/ -type ExpectedType = '*' | number -function destructure(node: SyntaxNode, expected: [ExpectedType]): [SyntaxNode] -function destructure( - node: SyntaxNode, - expected: [ExpectedType, ExpectedType] -): [SyntaxNode, SyntaxNode] -function destructure( - node: SyntaxNode, - expected: [ExpectedType, ExpectedType, ExpectedType] -): [SyntaxNode, SyntaxNode, SyntaxNode] -function destructure(node: SyntaxNode, expected: ExpectedType[]): SyntaxNode[] { +// Helper functions for extracting node parts +const getAllChildren = (node: SyntaxNode): SyntaxNode[] => { const children: SyntaxNode[] = [] let child = node.firstChild while (child) { children.push(child) child = child.nextSibling } + return children +} - if (children.length !== expected.length) { +const getBinaryParts = (node: SyntaxNode) => { + const children = getAllChildren(node) + const [left, op, right] = children + + if (!left || !op || !right) { + throw new RuntimeError(`BinOp expected 3 children, got ${children.length}`, node.from, node.to) + } + + return { left, op, right } +} + +const getAssignmentParts = (node: SyntaxNode) => { + const children = getAllChildren(node) + const [identifier, _equals, value] = children + + if (!identifier || !_equals || !value) { throw new RuntimeError( - `${node.type.name} expected ${expected.length} children, got ${children.length}`, + `Assignment expected 3 children, got ${children.length}`, node.from, node.to ) } - children.forEach((child, i) => { - const expectedType = expected[i] - if (expectedType !== '*' && child.type.id !== expectedType) { - throw new RuntimeError( - `Child ${i} of ${node.type.name} expected ${expectedType}, got ${child.type.id} (${child.type.name})`, - child.from, - child.to - ) - } - }) - - return children + return { identifier, value } +} + +const getParenParts = (node: SyntaxNode) => { + const children = getAllChildren(node) + const [_leftParen, expr, _rightParen] = children + + if (!_leftParen || !expr || !_rightParen) { + throw new RuntimeError( + `ParenExpr expected 3 children, got ${children.length}`, + node.from, + node.to + ) + } + + return expr +} + +const extractCommand = (node: SyntaxNode, input: string) => { + const children = getAllChildren(node) + const commandNode = children[0] // The Command node + + if (!commandNode || commandNode.type.id !== terms.Command) { + throw new RuntimeError('Invalid command structure', node.from, node.to) + } + + const commandName = input.slice(commandNode.firstChild!.from, commandNode.firstChild!.to) + const argNodes = children.slice(1) // All the Arg/NamedArg nodes + return { commandName, commandNode, argNodes } } diff --git a/src/utils/utils.tsx b/src/utils/utils.tsx index cd3733e..75eab6f 100644 --- a/src/utils/utils.tsx +++ b/src/utils/utils.tsx @@ -23,3 +23,7 @@ export const toElement = (node: any): HTMLElement => { render(node, c) return c.firstElementChild as HTMLElement } + +export const assertNever = (x: never): never => { + throw new Error(`Unexpected object: ${x}`) +}