diff --git a/src/editor/commands.ts b/src/editor/commands.ts index 499c230..021aefb 100644 --- a/src/editor/commands.ts +++ b/src/editor/commands.ts @@ -5,20 +5,13 @@ export type CommandShape = { execute: string | ((...args: any[]) => any) } -type ArgShape = - | { - name: string - type: T - description?: string - named?: false - } - | { - name: string - type: T - description?: string - named: true - default: ArgTypeMap[T] - } +type ArgShape = { + name: string + type: T + description?: string + optional?: boolean + default?: ArgTypeMap[T] +} type ArgTypeMap = { string: string diff --git a/src/evaluator/evaluator.test.ts b/src/evaluator/evaluator.test.ts index 36e0e60..efab176 100644 --- a/src/evaluator/evaluator.test.ts +++ b/src/evaluator/evaluator.test.ts @@ -67,10 +67,14 @@ test('simple command', () => { ] 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) => { try { setCommandSource(() => commands) diff --git a/src/evaluator/evaluator.ts b/src/evaluator/evaluator.ts index b1b8664..5f0700e 100644 --- a/src/evaluator/evaluator.ts +++ b/src/evaluator/evaluator.ts @@ -3,6 +3,7 @@ import * as terms from '../parser/shrimp.terms.ts' import { RuntimeError } from '#evaluator/runtimeError.ts' import { assert } from 'console' import { assertNever } from '#utils/utils.tsx' +import { matchingCommands, type CommandShape } from '#editor/commands.ts' export const evaluate = (input: string, tree: Tree, context: Context) => { 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 => { switch (evalNode.kind) { case 'number': @@ -81,15 +87,104 @@ const evaluateEvalNode = (evalNode: EvalNode, input: string, context: Context): } } - case 'arg': { - // Just evaluate the arg's value - return evaluateEvalNode(evalNode.value, input, context) + case 'function': { + const func = (...args: any[]) => { + 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': { - // TODO: Actually execute the command - // For now, just return undefined - return undefined + const { match: command } = matchingCommands(evalNode.name) + if (!command) { + 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: @@ -99,15 +194,19 @@ const evaluateEvalNode = (evalNode: EvalNode, input: string, context: Context): type Operators = '+' | '-' | '*' | '/' type Context = Map +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 = | { kind: 'number'; value: number; node: SyntaxNode } | { kind: 'string'; value: string; 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: '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: ArgEvalNode[]; node: SyntaxNode } + | { kind: 'function'; params: string[]; body: EvalNode; node: SyntaxNode } + | IdentifierEvalNode const syntaxNodeToEvalNode = (node: SyntaxNode, input: string, context: Context): EvalNode => { 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 } } + + 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) @@ -241,7 +370,8 @@ const extractCommand = (node: SyntaxNode, input: string) => { 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 return { commandName, commandNode, argNodes } } diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index fda2857..c33a3c6 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -1,5 +1,5 @@ @external propSource highlighting from "./highlight.js" -@top Program { line } +@top Program { line* } line { CommandCall semi | diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index 0ce3b13..d556989 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -4,7 +4,7 @@ export const Command = 2, CommandPartial = 3, UnquotedArg = 4, - insertedSemi = 31, + insertedSemi = 32, Program = 5, CommandCall = 6, NamedArg = 7, diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 45b62f9..1217c02 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -4,16 +4,16 @@ import {tokenizer, argTokenizer, insertSemicolon} from "./tokenizers" import {highlighting} from "./highlight.js" export const parser = LRParser.deserialize({ 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+$^", - 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~", - goto: "#rwPPPPPPx{PPPP!PP![P![P!dP![PPPPP{{!g!mPPPP!s!v!}#[#nRWOTfVgcUOTVY^_dgj]SOTY^_jR]QQgVRogQ[QRi[RXOSeVgRnd[SOTY^_jVcVdgQROQbTQhYQk^Ql_RpjTaRW", + 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: "'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: "$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", - maxTerm: 39, + maxTerm: 40, propSources: [highlighting], skippedNodes: [0], - repeatNodeCount: 2, - 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", + 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~!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], topRules: {"Program":[0,5]}, - tokenPrec: 266 + tokenPrec: 282 })