This commit is contained in:
Corey Johnson 2025-10-03 10:25:36 -07:00
parent 43e0b93a2a
commit d0ad8a0f20
6 changed files with 161 additions and 34 deletions

View File

@ -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

View File

@ -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)

View File

@ -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 }
} }

View File

@ -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 |

View File

@ -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,

View File

@ -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
}) })