wip
This commit is contained in:
parent
f608c9e4c5
commit
43e0b93a2a
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user