wip
This commit is contained in:
parent
f608c9e4c5
commit
43e0b93a2a
|
|
@ -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 => {
|
||||
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) {
|
||||
return { identifier, value }
|
||||
}
|
||||
|
||||
const getParenParts = (node: SyntaxNode) => {
|
||||
const children = getAllChildren(node)
|
||||
const [_leftParen, expr, _rightParen] = children
|
||||
|
||||
if (!_leftParen || !expr || !_rightParen) {
|
||||
throw new RuntimeError(
|
||||
`Child ${i} of ${node.type.name} expected ${expectedType}, got ${child.type.id} (${child.type.name})`,
|
||||
child.from,
|
||||
child.to
|
||||
`ParenExpr expected 3 children, got ${children.length}`,
|
||||
node.from,
|
||||
node.to
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return children
|
||||
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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user