This commit is contained in:
Corey Johnson 2025-10-03 09:13:58 -07:00
parent f608c9e4c5
commit 43e0b93a2a
2 changed files with 130 additions and 50 deletions

View File

@ -1,6 +1,8 @@
import { Tree, type SyntaxNode } from '@lezer/common' import { Tree, type SyntaxNode } from '@lezer/common'
import * as terms from '../parser/shrimp.terms.ts' import * as terms from '../parser/shrimp.terms.ts'
import { RuntimeError } from '#evaluator/runtimeError.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) => { export const evaluate = (input: string, tree: Tree, context: Context) => {
let result = undefined 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 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) { switch (evalNode.kind) {
case 'number': case 'number':
case 'string': case 'string':
@ -35,21 +49,20 @@ const evaluateNode = (node: SyntaxNode, input: string, context: Context): any =>
if (context.has(name)) { if (context.has(name)) {
return context.get(name) return context.get(name)
} else { } 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': { case 'assignment': {
const name = evalNode.name const name = evalNode.name
const value = evaluateNode(evalNode.value.node, input, context) const value = evaluateEvalNode(evalNode.value, input, context)
context.set(name, value) context.set(name, value)
return value return value
} }
case 'binop': { case 'binop': {
const left = evaluateNode(evalNode.left, input, context) const left = evaluateEvalNode(evalNode.left, input, context)
const right = evaluateNode(evalNode.right, input, context) const right = evaluateEvalNode(evalNode.right, input, context)
if (evalNode.op === '+') { if (evalNode.op === '+') {
return left + right return left + right
@ -60,9 +73,27 @@ const evaluateNode = (node: SyntaxNode, input: string, context: Context): any =>
} else if (evalNode.op === '/') { } else if (evalNode.op === '/') {
return left / right return left / right
} else { } 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: 'string'; value: string; node: SyntaxNode }
| { kind: 'boolean'; value: boolean; node: SyntaxNode } | { kind: 'boolean'; value: boolean; node: SyntaxNode }
| { kind: 'identifier'; name: string; 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: 'assignment'; name: string; value: EvalNode; node: SyntaxNode }
| { kind: 'arg'; name?: string; value: EvalNode; node: SyntaxNode }
| { kind: 'command'; name: string; args: EvalNode[]; node: SyntaxNode } | { kind: 'command'; name: string; args: EvalNode[]; node: SyntaxNode }
const syntaxNodeToEvalNode = (node: SyntaxNode, input: string, context: Context): EvalNode => { 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 } return { kind: 'identifier', name: value, node }
case terms.BinOp: { 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 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: { 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 name = input.slice(identifier.from, identifier.to)
const value = syntaxNodeToEvalNode(expr, input, context) const value = syntaxNodeToEvalNode(expr, input, context)
return { kind: 'assignment', name, value, node } return { kind: 'assignment', name, value, node }
} }
case terms.ParenExpr: { case terms.ParenExpr: {
const [_leftParen, expr, _rightParen] = destructure(node, ['*', '*', '*']) const expr = getParenParts(node)
return syntaxNodeToEvalNode(expr, input, context) return syntaxNodeToEvalNode(expr, input, context)
} }
case terms.CommandCall: { case terms.CommandCall: {
const [_at, identifier, _leftParen, ...rest] = destructure(node, [ const { commandName, argNodes } = extractCommand(node, input)
'*',
terms.Identifier, 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) throw new RuntimeError(`Unsupported node type "${node.type.name}"`, node.from, node.to)
} }
/* // Helper functions for extracting node parts
The code below is a... const getAllChildren = (node: SyntaxNode): SyntaxNode[] => {
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[] {
const children: SyntaxNode[] = [] const children: SyntaxNode[] = []
let child = node.firstChild let child = node.firstChild
while (child) { while (child) {
children.push(child) children.push(child)
child = child.nextSibling 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( throw new RuntimeError(
`${node.type.name} expected ${expected.length} children, got ${children.length}`, `Assignment expected 3 children, got ${children.length}`,
node.from, node.from,
node.to node.to
) )
} }
children.forEach((child, i) => { return { identifier, value }
const expectedType = expected[i] }
if (expectedType !== '*' && child.type.id !== expectedType) {
throw new RuntimeError( const getParenParts = (node: SyntaxNode) => {
`Child ${i} of ${node.type.name} expected ${expectedType}, got ${child.type.id} (${child.type.name})`, const children = getAllChildren(node)
child.from, const [_leftParen, expr, _rightParen] = children
child.to
) if (!_leftParen || !expr || !_rightParen) {
} throw new RuntimeError(
}) `ParenExpr expected 3 children, got ${children.length}`,
node.from,
return children 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 }
} }

View File

@ -23,3 +23,7 @@ export const toElement = (node: any): HTMLElement => {
render(node, c) render(node, c)
return c.firstElementChild as HTMLElement return c.firstElementChild as HTMLElement
} }
export const assertNever = (x: never): never => {
throw new Error(`Unexpected object: ${x}`)
}