wip
This commit is contained in:
parent
43e0b93a2a
commit
d0ad8a0f20
|
|
@ -5,19 +5,12 @@ export type CommandShape = {
|
|||
execute: string | ((...args: any[]) => any)
|
||||
}
|
||||
|
||||
type ArgShape<T extends keyof ArgTypeMap = keyof ArgTypeMap> =
|
||||
| {
|
||||
type ArgShape<T extends keyof ArgTypeMap = keyof ArgTypeMap> = {
|
||||
name: string
|
||||
type: T
|
||||
description?: string
|
||||
named?: false
|
||||
}
|
||||
| {
|
||||
name: string
|
||||
type: T
|
||||
description?: string
|
||||
named: true
|
||||
default: ArgTypeMap[T]
|
||||
optional?: boolean
|
||||
default?: ArgTypeMap[T]
|
||||
}
|
||||
|
||||
type ArgTypeMap = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<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 =
|
||||
| { 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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
@external propSource highlighting from "./highlight.js"
|
||||
@top Program { line }
|
||||
@top Program { line* }
|
||||
|
||||
line {
|
||||
CommandCall semi |
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export const
|
|||
Command = 2,
|
||||
CommandPartial = 3,
|
||||
UnquotedArg = 4,
|
||||
insertedSemi = 31,
|
||||
insertedSemi = 32,
|
||||
Program = 5,
|
||||
CommandCall = 6,
|
||||
NamedArg = 7,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user