wip
This commit is contained in:
parent
43e0b93a2a
commit
d0ad8a0f20
|
|
@ -5,20 +5,13 @@ export type CommandShape = {
|
||||||
execute: string | ((...args: any[]) => any)
|
execute: string | ((...args: any[]) => any)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArgShape<T extends keyof ArgTypeMap = keyof ArgTypeMap> =
|
type ArgShape<T extends keyof ArgTypeMap = keyof ArgTypeMap> = {
|
||||||
| {
|
name: string
|
||||||
name: string
|
type: T
|
||||||
type: T
|
description?: string
|
||||||
description?: string
|
optional?: boolean
|
||||||
named?: false
|
default?: ArgTypeMap[T]
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: string
|
|
||||||
type: T
|
|
||||||
description?: string
|
|
||||||
named: true
|
|
||||||
default: ArgTypeMap[T]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArgTypeMap = {
|
type ArgTypeMap = {
|
||||||
string: string
|
string: string
|
||||||
|
|
|
||||||
|
|
@ -67,10 +67,14 @@ test('simple command', () => {
|
||||||
]
|
]
|
||||||
|
|
||||||
withCommands(commands, () => {
|
withCommands(commands, () => {
|
||||||
expect(`echo hello`).toEvaluateTo('hello')
|
expect(`echo 'hello'`).toEvaluateTo('hello')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.only('function', () => {
|
||||||
|
expect(`add = fn a b: a + b; add 2 4`).toEvaluateTo(5)
|
||||||
|
})
|
||||||
|
|
||||||
const withCommands = (commands: CommandShape[], fn: () => void) => {
|
const withCommands = (commands: CommandShape[], fn: () => void) => {
|
||||||
try {
|
try {
|
||||||
setCommandSource(() => commands)
|
setCommandSource(() => commands)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ 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 { assert } from 'console'
|
||||||
import { assertNever } from '#utils/utils.tsx'
|
import { assertNever } from '#utils/utils.tsx'
|
||||||
|
import { matchingCommands, type CommandShape } from '#editor/commands.ts'
|
||||||
|
|
||||||
export const evaluate = (input: string, tree: Tree, context: Context) => {
|
export const evaluate = (input: string, tree: Tree, context: Context) => {
|
||||||
let result = undefined
|
let result = undefined
|
||||||
|
|
@ -37,6 +38,11 @@ const evaluateNode = (node: SyntaxNode, input: string, context: Context): any =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResolvedArg = {
|
||||||
|
value: any
|
||||||
|
resolved: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const evaluateEvalNode = (evalNode: EvalNode, input: string, context: Context): any => {
|
const evaluateEvalNode = (evalNode: EvalNode, input: string, context: Context): any => {
|
||||||
switch (evalNode.kind) {
|
switch (evalNode.kind) {
|
||||||
case 'number':
|
case 'number':
|
||||||
|
|
@ -81,15 +87,104 @@ const evaluateEvalNode = (evalNode: EvalNode, input: string, context: Context):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'arg': {
|
case 'function': {
|
||||||
// Just evaluate the arg's value
|
const func = (...args: any[]) => {
|
||||||
return evaluateEvalNode(evalNode.value, input, context)
|
if (args.length !== evalNode.params.length) {
|
||||||
|
throw new RuntimeError(
|
||||||
|
`Function expected ${evalNode.params.length} arguments, got ${args.length}`,
|
||||||
|
evalNode.node.from,
|
||||||
|
evalNode.node.to
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new context with parameter bindings
|
||||||
|
const localContext = new Map(context)
|
||||||
|
evalNode.params.forEach((param, index) => {
|
||||||
|
localContext.set(param, args[index])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Evaluate function body with new context
|
||||||
|
return evaluateEvalNode(evalNode.body, input, localContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'command': {
|
case 'command': {
|
||||||
// TODO: Actually execute the command
|
const { match: command } = matchingCommands(evalNode.name)
|
||||||
// For now, just return undefined
|
if (!command) {
|
||||||
return undefined
|
const { from, to } = evalNode.node
|
||||||
|
throw new RuntimeError(`Unknown command "${evalNode.name}"`, from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedArgs: ResolvedArg[] = command.args.map((argShape) => ({
|
||||||
|
value: argShape.default,
|
||||||
|
resolved: argShape.optional ? true : argShape.default !== undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Filter the args into named and positional
|
||||||
|
const namedArgNodes: NamedArgEvalNode[] = []
|
||||||
|
const positionalArgNodes: PositionalArgEvalNode[] = []
|
||||||
|
evalNode.args.forEach((arg) => {
|
||||||
|
const isNamedArg = 'name' in arg && arg.name !== undefined
|
||||||
|
isNamedArg ? namedArgNodes.push(arg) : positionalArgNodes.push(arg)
|
||||||
|
})
|
||||||
|
|
||||||
|
// First set the named args
|
||||||
|
namedArgNodes.forEach((arg) => {
|
||||||
|
const shapeIndex = command.args.findIndex((def) => def.name === arg.name)
|
||||||
|
const shape = command.args[shapeIndex]
|
||||||
|
|
||||||
|
if (!shape) {
|
||||||
|
const { from, to } = arg.node
|
||||||
|
throw new RuntimeError(`Unknown argument "${arg.name}"`, from, to)
|
||||||
|
} else if (resolvedArgs[shapeIndex]?.resolved) {
|
||||||
|
const { from, to } = arg.node
|
||||||
|
throw new RuntimeError(`Argument "${arg.name}" already set`, from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = evaluateEvalNode(arg.value, input, context)
|
||||||
|
resolvedArgs[shapeIndex] = { value, resolved: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Now set the positional args in order
|
||||||
|
let unresolvedIndex = resolvedArgs.findIndex((arg) => !arg.resolved)
|
||||||
|
positionalArgNodes.forEach((arg) => {
|
||||||
|
const value = evaluateEvalNode(arg.value, input, context)
|
||||||
|
if (unresolvedIndex === -1) {
|
||||||
|
const { from, to } = arg.node
|
||||||
|
throw new RuntimeError(`Too many positional arguments`, from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedArgs[unresolvedIndex] = { value, resolved: true }
|
||||||
|
unresolvedIndex = resolvedArgs.findIndex((arg) => !arg.resolved)
|
||||||
|
})
|
||||||
|
|
||||||
|
let executor
|
||||||
|
if (typeof command.execute === 'string') {
|
||||||
|
throw new RuntimeError(
|
||||||
|
`Path-based commands aren't supported yet...`,
|
||||||
|
evalNode.node.from,
|
||||||
|
evalNode.node.to
|
||||||
|
)
|
||||||
|
// Dynamic imports are not supported in Bun test environment
|
||||||
|
// See:
|
||||||
|
// const { default: importedExecutor } = await import(command.execute)
|
||||||
|
// executor = importedExecutor
|
||||||
|
// if (typeof executor !== 'function') {
|
||||||
|
// throw new RuntimeError(
|
||||||
|
// `Module "${command.execute}" for command ${command.command} does not export a default function`,
|
||||||
|
// evalNode.node.from,
|
||||||
|
// evalNode.node.to
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
executor = command.execute
|
||||||
|
}
|
||||||
|
|
||||||
|
const argValues = resolvedArgs.map((arg) => arg.value)
|
||||||
|
const result = executor(...argValues)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
@ -99,15 +194,19 @@ const evaluateEvalNode = (evalNode: EvalNode, input: string, context: Context):
|
||||||
|
|
||||||
type Operators = '+' | '-' | '*' | '/'
|
type Operators = '+' | '-' | '*' | '/'
|
||||||
type Context = Map<string, any>
|
type Context = Map<string, any>
|
||||||
|
type NamedArgEvalNode = { kind: 'arg'; value: EvalNode; name: string; node: SyntaxNode }
|
||||||
|
type PositionalArgEvalNode = { kind: 'arg'; value: EvalNode; node: SyntaxNode }
|
||||||
|
type ArgEvalNode = NamedArgEvalNode | PositionalArgEvalNode
|
||||||
|
type IdentifierEvalNode = { kind: 'identifier'; name: string; node: SyntaxNode }
|
||||||
type EvalNode =
|
type EvalNode =
|
||||||
| { kind: 'number'; value: number; node: SyntaxNode }
|
| { kind: 'number'; value: number; node: SyntaxNode }
|
||||||
| { 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: 'binop'; op: Operators; left: EvalNode; right: EvalNode; 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: ArgEvalNode[]; node: SyntaxNode }
|
||||||
| { kind: 'command'; name: string; args: EvalNode[]; node: SyntaxNode }
|
| { kind: 'function'; params: string[]; body: EvalNode; node: SyntaxNode }
|
||||||
|
| IdentifierEvalNode
|
||||||
|
|
||||||
const syntaxNodeToEvalNode = (node: SyntaxNode, input: string, context: Context): EvalNode => {
|
const syntaxNodeToEvalNode = (node: SyntaxNode, input: string, context: Context): EvalNode => {
|
||||||
const value = input.slice(node.from, node.to)
|
const value = input.slice(node.from, node.to)
|
||||||
|
|
@ -176,6 +275,36 @@ const syntaxNodeToEvalNode = (node: SyntaxNode, input: string, context: Context)
|
||||||
|
|
||||||
return { kind: 'command', name: commandName, args, node }
|
return { kind: 'command', name: commandName, args, node }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case terms.Function: {
|
||||||
|
const children = getAllChildren(node)
|
||||||
|
if (children.length < 3) {
|
||||||
|
throw new Error(
|
||||||
|
`Parser bug: Function node has ${children.length} children, expected at least 3`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structure: fn params : body
|
||||||
|
const [_fn, paramsNode, _colon, ...bodyNodes] = children
|
||||||
|
|
||||||
|
// Extract parameter names
|
||||||
|
const paramNodes = getAllChildren(paramsNode)
|
||||||
|
const params = paramNodes.map((paramNode) => {
|
||||||
|
if (paramNode.type.id !== terms.Identifier) {
|
||||||
|
throw new Error(`Parser bug: Function parameter is not an identifier`)
|
||||||
|
}
|
||||||
|
return input.slice(paramNode.from, paramNode.to)
|
||||||
|
})
|
||||||
|
|
||||||
|
// For now, assume body is a single expression (the rest of the children)
|
||||||
|
const bodyNode = bodyNodes[0]
|
||||||
|
if (!bodyNode) {
|
||||||
|
throw new Error(`Parser bug: Function missing body`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = syntaxNodeToEvalNode(bodyNode, input, context)
|
||||||
|
return { kind: 'function', params, body, 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)
|
||||||
|
|
@ -241,7 +370,8 @@ const extractCommand = (node: SyntaxNode, input: string) => {
|
||||||
throw new RuntimeError('Invalid command structure', node.from, node.to)
|
throw new RuntimeError('Invalid command structure', node.from, node.to)
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandName = input.slice(commandNode.firstChild!.from, commandNode.firstChild!.to)
|
const commandNameNode = commandNode.firstChild ?? commandNode
|
||||||
|
const commandName = input.slice(commandNameNode.from, commandNameNode.to)
|
||||||
const argNodes = children.slice(1) // All the Arg/NamedArg nodes
|
const argNodes = children.slice(1) // All the Arg/NamedArg nodes
|
||||||
return { commandName, commandNode, argNodes }
|
return { commandName, commandNode, argNodes }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
@external propSource highlighting from "./highlight.js"
|
@external propSource highlighting from "./highlight.js"
|
||||||
@top Program { line }
|
@top Program { line* }
|
||||||
|
|
||||||
line {
|
line {
|
||||||
CommandCall semi |
|
CommandCall semi |
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ export const
|
||||||
Command = 2,
|
Command = 2,
|
||||||
CommandPartial = 3,
|
CommandPartial = 3,
|
||||||
UnquotedArg = 4,
|
UnquotedArg = 4,
|
||||||
insertedSemi = 31,
|
insertedSemi = 32,
|
||||||
Program = 5,
|
Program = 5,
|
||||||
CommandCall = 6,
|
CommandCall = 6,
|
||||||
NamedArg = 7,
|
NamedArg = 7,
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,16 @@ import {tokenizer, argTokenizer, insertSemicolon} from "./tokenizers"
|
||||||
import {highlighting} from "./highlight.js"
|
import {highlighting} from "./highlight.js"
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "%^OkQTOOOuQaO'#DQO!aQTO'#ClO!iQaO'#DOOOQ`'#DR'#DROVQTO'#ChOOQl'#DQ'#DQO#fQnO'#CbO!uQaO'#DOQOQPOOOVQTO,59UOOQS'#Cy'#CyO#pQTO'#CnO#xQPO,59WOVQTO,59[OVQTO,59[OOQO'#DS'#DSOOQO,59j,59jO#}QPO,59SOOQl'#DP'#DPO$tQnO'#CvOOQl'#Cw'#CwOOQl'#Cx'#CxO%RQnO,58|O%]QaO1G.pOOQS-E6w-E6wOVQTO1G.rOOQ`1G.v1G.vO%tQaO1G.vOOQl1G.n1G.nOOQl,58},58}OOQl-E6v-E6vO&]QaO7+$^",
|
states: "%jQVQTOOOqQaO'#DRO!]QTO'#ClO!eQaO'#DPOOQ`'#DS'#DSO!yQTO'#ChOOQl'#DR'#DRO#vQnO'#CbO!qQaO'#DPOOQS'#Cx'#CxQVQTOOO!yQTO,59UOOQS'#Cz'#CzO$QQTO'#CnO$YQPO,59WO!yQTO,59[O!yQTO,59[OOQS'#DT'#DTOOQS,59k,59kO$_QPO,59SOOQl'#DQ'#DQO%UQnO'#CvOOQl'#Cw'#CwOOQl'#Cy'#CyO%cQnO,58|OOQS-E6v-E6vO%mQaO1G.pOOQS-E6x-E6xO!yQTO1G.rOOQ`1G.v1G.vO&UQaO1G.vOOQl1G.n1G.nOOQl,58},58}OOQl-E6w-E6wO&mQaO7+$^",
|
||||||
stateData: "&w~OqOS~OPPOXUOYUOZUO]TOaQO~OQVORVO~PVO_YOetXftXgtXhtXotXwtXitX~OPZOcbP~Oe^Of^Og_Oh_Oo`Ow`O~OPUOScOWdOXUOYUOZUO]TO~OoUXwUX~P!}OPZOcbX~OcjO~Oe^Of^Og_Oh_OimO~OPUOScOXUOYUOZUO]TO~OWjXojXwjX~P$`OoUawUa~P!}Oe^Of^Og_Oh_Oo^iw^ii^i~Oe^Of^Ogdihdiodiwdiidi~Oe^Of^Og_Oh_Oo`qw`qi`q~OXh~",
|
stateData: "'X~OrOS~OPPOQVORVOXUOYUOZUO]TOaQO~O_ZOeuXfuXguXhuXpuXxuXiuX~OP[OcbP~Oe_Of_Og`Oh`OpaOxaO~OPPOXUOYUOZUO]TOaQO~OPUOSdOWeOXUOYUOZUO]TO~OpUXxUX~P#_OP[OcbX~OclO~Oe_Of_Og`Oh`OioO~OPUOSdOXUOYUOZUO]TO~OWjXpjXxjX~P$pOpUaxUa~P#_Oe_Of_Og`Oh`Op^ix^ii^i~Oe_Of_Ogdihdipdixdiidi~Oe_Of_Og`Oh`Op`qx`qi`q~OXh~",
|
||||||
goto: "#rwPPPPPPx{PPPP!PP![P![P!dP![PPPPP{{!g!mPPPP!s!v!}#[#nRWOTfVgcUOTVY^_dgj]SOTY^_jR]QQgVRogQ[QRi[RXOSeVgRnd[SOTY^_jVcVdgQROQbTQhYQk^Ql_RpjTaRW",
|
goto: "$PxPPPPPPy}PPPP!RP!_P!_P!hP!_PPPPP}}!k!q!wPPPP!}#R#Y#h#{TWOYTgVheUOTVYZ_`ehl_SOTYZ_`lR^QQYORiYQhVRqhQ]QRk]TXOYSfVhRpe^SOTYZ_`lVdVehSROYQcTQjZQm_Qn`RrlTbRW",
|
||||||
nodeNames: "⚠ Identifier Command CommandPartial UnquotedArg Program CommandCall NamedArg NamedArgPrefix Number String Boolean ParenExpr paren Assignment operator Function keyword Params colon BinOp operator operator operator operator paren PartialNamedArg Arg",
|
nodeNames: "⚠ Identifier Command CommandPartial UnquotedArg Program CommandCall NamedArg NamedArgPrefix Number String Boolean ParenExpr paren Assignment operator Function keyword Params colon BinOp operator operator operator operator paren PartialNamedArg Arg",
|
||||||
maxTerm: 39,
|
maxTerm: 40,
|
||||||
propSources: [highlighting],
|
propSources: [highlighting],
|
||||||
skippedNodes: [0],
|
skippedNodes: [0],
|
||||||
repeatNodeCount: 2,
|
repeatNodeCount: 3,
|
||||||
tokenData: "*W~RjX^!spq!swx#hxy$lyz$qz{$v{|${}!O%Q!P!Q%s!Q![%Y![!]%x!]!^%}!_!`&S#T#Y&X#Y#Z&m#Z#h&X#h#i)[#i#o&X#y#z!s$f$g!s#BY#BZ!s$IS$I_!s$I|$JO!s$JT$JU!s$KV$KW!s&FU&FV!s~!xYq~X^!spq!s#y#z!s$f$g!s#BY#BZ!s$IS$I_!s$I|$JO!s$JT$JU!s$KV$KW!s&FU&FV!s~#kUOr#hsw#hwx#}x;'S#h;'S;=`$f<%lO#h~$SUY~Or#hsw#hwx#}x;'S#h;'S;=`$f<%lO#h~$iP;=`<%l#h~$qO]~~$vOi~~${Oe~~%QOg~~%VPh~!Q![%Y~%_QX~!O!P%e!Q![%Y~%hP!Q![%k~%pPX~!Q![%k~%xOf~~%}Oc~~&SOw~~&XO_~Q&[S}!O&X!Q![&X!_!`&h#T#o&XQ&mOWQ~&pV}!O&X!Q![&X!_!`&h#T#U'V#U#b&X#b#c(y#c#o&X~'YU}!O&X!Q![&X!_!`&h#T#`&X#`#a'l#a#o&X~'oU}!O&X!Q![&X!_!`&h#T#g&X#g#h(R#h#o&X~(UU}!O&X!Q![&X!_!`&h#T#X&X#X#Y(h#Y#o&X~(mSZ~}!O&X!Q![&X!_!`&h#T#o&XR)OSaP}!O&X!Q![&X!_!`&h#T#o&X~)_U}!O&X!Q![&X!_!`&h#T#f&X#f#g)q#g#o&X~)tU}!O&X!Q![&X!_!`&h#T#i&X#i#j(R#j#o&X",
|
tokenData: "*W~RjX^!spq!swx#hxy$lyz$qz{$v{|${}!O%Q!P!Q%s!Q![%Y![!]%x!]!^%}!_!`&S#T#Y&X#Y#Z&m#Z#h&X#h#i)[#i#o&X#y#z!s$f$g!s#BY#BZ!s$IS$I_!s$I|$JO!s$JT$JU!s$KV$KW!s&FU&FV!s~!xYr~X^!spq!s#y#z!s$f$g!s#BY#BZ!s$IS$I_!s$I|$JO!s$JT$JU!s$KV$KW!s&FU&FV!s~#kUOr#hsw#hwx#}x;'S#h;'S;=`$f<%lO#h~$SUY~Or#hsw#hwx#}x;'S#h;'S;=`$f<%lO#h~$iP;=`<%l#h~$qO]~~$vOi~~${Oe~~%QOg~~%VPh~!Q![%Y~%_QX~!O!P%e!Q![%Y~%hP!Q![%k~%pPX~!Q![%k~%xOf~~%}Oc~~&SOx~~&XO_~Q&[S}!O&X!Q![&X!_!`&h#T#o&XQ&mOWQ~&pV}!O&X!Q![&X!_!`&h#T#U'V#U#b&X#b#c(y#c#o&X~'YU}!O&X!Q![&X!_!`&h#T#`&X#`#a'l#a#o&X~'oU}!O&X!Q![&X!_!`&h#T#g&X#g#h(R#h#o&X~(UU}!O&X!Q![&X!_!`&h#T#X&X#X#Y(h#Y#o&X~(mSZ~}!O&X!Q![&X!_!`&h#T#o&XR)OSaP}!O&X!Q![&X!_!`&h#T#o&X~)_U}!O&X!Q![&X!_!`&h#T#f&X#f#g)q#g#o&X~)tU}!O&X!Q![&X!_!`&h#T#i&X#i#j(R#j#o&X",
|
||||||
tokenizers: [0, 1, tokenizer, argTokenizer, insertSemicolon],
|
tokenizers: [0, 1, tokenizer, argTokenizer, insertSemicolon],
|
||||||
topRules: {"Program":[0,5]},
|
topRules: {"Program":[0,5]},
|
||||||
tokenPrec: 266
|
tokenPrec: 282
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user