This commit is contained in:
Corey Johnson 2025-10-13 15:35:58 -07:00
parent 1f147d2d26
commit 3a12d7baff
9 changed files with 186 additions and 194 deletions

View File

@ -70,6 +70,17 @@ describe('compiler', () => {
expect(`add = fn a b: a + b end; add 2 9`).toEvaluateTo(11)
})
test('function call with named args', () => {
expect(`minus = fn a b: a - b end; minus b=2 a=9`).toEvaluateTo(7)
})
test('function call with named and positional args', () => {
expect(`minus = fn a b: a - b end; minus b=2 9`).toEvaluateTo(7)
expect(`minus = fn c d: a - b end; minus 90 b=20`).toEvaluateTo(70)
expect(`minus = fn e f: a - b end; minus a=900 200`).toEvaluateTo(700)
expect(`minus = fn g h: a - b end; minus 2000 a=9000`).toEvaluateTo(7000)
})
test('function call with no args', () => {
expect(`bloop = fn: 'bloop' end; bloop`).toEvaluateTo('bloop')
})
@ -136,7 +147,7 @@ describe('errors', () => {
})
})
describe.skip('multiline tests', () => {
describe('multiline tests', () => {
test('multiline function', () => {
expect(`
add = fn a b:

View File

@ -13,8 +13,12 @@ import {
getFunctionDefParts,
getIfExprParts,
getNamedArgParts,
getPipeExprParts,
} from '#compiler/utils'
const DEBUG = false
// const DEBUG = true
type Label = `.${string}`
export class Compiler {
instructions: ProgramItem[] = []
@ -25,14 +29,21 @@ export class Compiler {
constructor(public input: string) {
try {
const cst = parser.parse(input)
const errors = checkTreeForErrors(cst, input)
const errors = checkTreeForErrors(cst)
if (errors.length > 0) {
throw new CompilerError(`Syntax errors found:\n${errors.join('\n')}`, 0, input.length)
const firstError = errors[0]
if (firstError) {
throw firstError
}
this.#compileCst(cst, input)
throw new CompilerError(
'I am a very long fake error to test scrolling and other things this is super long\nand has multiple lines\nand just keeps going and going and going',
20,
25
)
// Add the labels
for (const [label, labelInstructions] of this.fnLabels) {
this.instructions.push([`${label}:`])
@ -40,7 +51,7 @@ export class Compiler {
this.instructions.push(['RETURN'])
}
// logInstructions(this.instructions)
if (DEBUG) logInstructions(this.instructions)
this.bytecode = toBytecode(this.instructions)
} catch (error) {
@ -67,6 +78,9 @@ export class Compiler {
#compileNode(node: SyntaxNode, input: string): ProgramItem[] {
const value = input.slice(node.from, node.to)
if (DEBUG) console.log(`🫦 ${node.name}: ${value}`)
switch (node.type.id) {
case terms.Number:
const number = Number(value)
@ -289,123 +303,61 @@ export class Compiler {
}
case terms.PipeExpr: {
const allChildren = getAllChildren(node)
// Filter out the pipe operator nodes (they're just syntax)
const operands = allChildren.filter((child) => child.type.name !== 'operator')
if (operands.length < 2) {
const { pipedFunctionCall, pipeReceivers } = getPipeExprParts(node)
if (!pipedFunctionCall || pipeReceivers.length === 0) {
throw new CompilerError('PipeExpr must have at least two operands', node.from, node.to)
}
const instructions: ProgramItem[] = []
instructions.push(...this.#compileNode(pipedFunctionCall, input))
// Compile first operand normally
instructions.push(...this.#compileNode(operands[0]!, input))
pipeReceivers.forEach((pipeReceiver) => {
instructions.push(['STORE', '_pipe_value'])
// For each subsequent operand, transform it to receive piped value as first arg
for (let i = 1; i < operands.length; i++) {
const operand = operands[i]!
const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts(
pipeReceiver,
input
)
// Result from previous stage is on stack
// We need to make it the first argument to the next call
instructions.push(...this.#compileNode(identifierNode, input))
if (operand.type.id === terms.FunctionCallOrIdentifier) {
// Simple identifier - emit TRY_CALL with piped value as single argument
const identifierNode = operand.getChild('Identifier')
if (!identifierNode) {
throw new CompilerError('FunctionCallOrIdentifier must have Identifier', operand.from, operand.to)
}
const fnName = input.slice(identifierNode.from, identifierNode.to)
const isUnderscoreInPositionalArgs = positionalArgs.some(
(arg) => arg.type.id === terms.Underscore
)
const isUnderscoreInNamedArgs = namedArgs.some((arg) => {
const { valueNode } = getNamedArgParts(arg, input)
return valueNode.type.id === terms.Underscore
})
// Stack has: [piped_value]
// Store piped value temporarily
instructions.push(['STORE', '__pipe_value'])
const shouldPushPositionalArg = !isUnderscoreInPositionalArgs && !isUnderscoreInNamedArgs
// Load function
instructions.push(['TRY_LOAD', fnName])
// Load piped value as first arg
instructions.push(['LOAD', '__pipe_value'])
// Call with 1 positional arg and 0 named args
instructions.push(['PUSH', 1])
instructions.push(['PUSH', 0])
instructions.push(['CALL'])
} else if (operand.type.id === terms.FunctionCall) {
// Function call with arguments - check for underscore placeholder
const { identifierNode, namedArgs, positionalArgs } = getFunctionCallParts(operand, input)
// Check if any positional arg is an underscore placeholder
let underscoreIndex = -1
for (let j = 0; j < positionalArgs.length; j++) {
const arg = positionalArgs[j]!
const argValue = input.slice(arg.from, arg.to)
if (argValue === '_') {
underscoreIndex = j
break
}
}
if (underscoreIndex !== -1) {
// Underscore found - replace it with piped value at that position
// Store piped value temporarily
instructions.push(['STORE', '__pipe_value'])
// Load function
instructions.push(...this.#compileNode(identifierNode, input))
// Push positional args, replacing underscore with piped value
for (let j = 0; j < positionalArgs.length; j++) {
if (j === underscoreIndex) {
instructions.push(['LOAD', '__pipe_value'])
} else {
instructions.push(...this.#compileNode(positionalArgs[j]!, input))
}
}
// Push named args
namedArgs.forEach((arg) => {
const { name, valueNode } = getNamedArgParts(arg, input)
instructions.push(['PUSH', name])
instructions.push(...this.#compileNode(valueNode, input))
})
// Call with positionalArgs.length and namedArgs
instructions.push(['PUSH', positionalArgs.length])
instructions.push(['PUSH', namedArgs.length])
instructions.push(['CALL'])
} else {
// No underscore - piped value becomes first argument
// Store piped value temporarily
instructions.push(['STORE', '__pipe_value'])
// Load function
instructions.push(...this.#compileNode(identifierNode, input))
// Push piped value as first arg
instructions.push(['LOAD', '__pipe_value'])
// Push remaining positional args
positionalArgs.forEach((arg) => {
instructions.push(...this.#compileNode(arg, input))
})
// Push named args
namedArgs.forEach((arg) => {
const { name, valueNode } = getNamedArgParts(arg, input)
instructions.push(['PUSH', name])
instructions.push(...this.#compileNode(valueNode, input))
})
// Call with (positionalArgs + 1 for piped value) and namedArgs
instructions.push(['PUSH', positionalArgs.length + 1])
instructions.push(['PUSH', namedArgs.length])
instructions.push(['CALL'])
}
} else {
throw new CompilerError(`Unsupported pipe operand type: ${operand.type.name}`, operand.from, operand.to)
// If no underscore is explicitly used, add the piped value as the first positional arg
if (shouldPushPositionalArg) {
instructions.push(['LOAD', '_pipe_value'])
}
}
positionalArgs.forEach((arg) => {
if (arg.type.id === terms.Underscore) {
instructions.push(['LOAD', '_pipe_value'])
} else {
instructions.push(...this.#compileNode(arg, input))
}
})
namedArgs.forEach((arg) => {
const { name, valueNode } = getNamedArgParts(arg, input)
instructions.push(['PUSH', name])
if (valueNode.type.id === terms.Underscore) {
instructions.push(['LOAD', '_pipe_value'])
} else {
instructions.push(...this.#compileNode(valueNode, input))
}
})
instructions.push(['PUSH', positionalArgs.length + (shouldPushPositionalArg ? 1 : 0)])
instructions.push(['PUSH', namedArgs.length])
instructions.push(['CALL'])
})
return instructions
}

View File

@ -1,6 +1,11 @@
export class CompilerError extends Error {
constructor(message: string, private from: number, private to: number) {
super(message)
if (from < 0 || to < 0 || to < from) {
throw new Error(`Invalid CompilerError positions: from=${from}, to=${to}`)
}
this.name = 'CompilerError'
this.message = message
}
@ -19,27 +24,24 @@ export class CompilerError extends Error {
const lines = previousSevenLines
.map((line, index) => {
const currentLineNumber = lineNumber - previousSevenLines.length + index + 1
// repace leading whitespace with barely visible characters so they show up in terminal
line = line.replace(/^\s+/, (ws) => ws.replace(/ /g, green('·')).replace(/\t/g, '→ '))
return `${grey(currentLineNumber.toString().padStart(padding))}${line}`
})
.join('\n')
const underlineStartLen = (columnEnd - columnStart) / 2
const underlineEndLen = columnEnd - columnStart - underlineStartLen
const underline =
' '.repeat(columnStart - 1) +
'─'.repeat(underlineStartLen) +
'┬' +
'─'.repeat(underlineEndLen)
const underlineLen = columnEnd - columnStart + 1
const underline = ' '.repeat(columnStart - 1) + red('═'.repeat(underlineLen))
const messageWithArrow =
' '.repeat(columnStart + underlineStartLen - 1) + '╰── ' + blue(this.message)
const messageWithArrow = blue(this.message)
const message = `${green('')}
${ws} ${red('Compiler Error')}
${ws}
${lines}
${ws} ${underline}
${ws} ${messageWithArrow}
${ws} ${messageWithArrow.split('\n').join(`\n${ws}`)}
${ws}
${ws}
`
@ -54,7 +56,7 @@ ${ws}╰───
const line = lines[i]!
if (this.from >= currentPos && this.from <= currentPos + line.length) {
const columnStart = this.from - currentPos + 1
const columnEnd = columnStart + (this.to - this.from) - 1
const columnEnd = columnStart + (this.to - this.from)
// If the error spans multiple lines, so just return the line start
if (columnEnd > line.length) {

View File

@ -2,60 +2,80 @@ import { describe, test, expect } from 'bun:test'
describe('pipe expressions', () => {
test('simple pipe passes result as first argument', () => {
const code = `double = fn x: x * 2 end; result = 5 | double; result`
const code = `
double = fn x: x * 2 end
double 2 | double`
expect(code).toEvaluateTo(10)
expect(code).toEvaluateTo(8)
})
test('pipe chain with three stages', () => {
const code = `add-one = fn x: x + 1 end; double = fn x: x * 2 end; square = fn x: x * x end; result = 3 | add-one | double | square; result`
// 3 -> 4 -> 8 -> 64
expect(code).toEvaluateTo(64)
const code = `
add-one = fn x: x + 1 end
double = fn x: x * 2 end
minus-point-one = fn x: x - 0.1 end
add-one 3 | double | minus-point-one`
// 4 8 7.9
expect(code).toEvaluateTo(7.9)
})
test.skip('pipe with function that has additional arguments', () => {
// TODO: This test reveals a bug where functions with 2+ parameters
// don't properly bind all arguments. This is a general Shrimp issue,
// not specific to pipes. Skipping until the broader issue is fixed.
const code = `multiply = fn a b: a * b end; result = 5 | multiply 3; result`
test('pipe with function that has additional arguments', () => {
const code = `
multiply = fn a b: a * b end
get-five = fn: 5 end
get-five | multiply 3`
// 5 becomes first arg, 3 is second arg: 5 * 3 = 15
expect(code).toEvaluateTo(15)
})
test('pipe with bare identifier', () => {
const code = `get-value = 42; process = fn x: x + 10 end; result = get-value | process; result`
const code = `
get-value = 42
process = fn x: x + 10 end
get-value | process`
expect(code).toEvaluateTo(52)
})
test('pipe in assignment', () => {
const code = `add-ten = fn x: x + 10 end; result = 5 | add-ten; result`
const code = `
add-ten = fn x: x + 10 end
result = add-ten 5 | add-ten
result`
expect(code).toEvaluateTo(15)
// 5 + 10 = 15, then 15 + 10 = 25
expect(code).toEvaluateTo(25)
})
test.skip('pipe with underscore placeholder', () => {
// TODO: This test depends on the fix for two-parameter functions
// which is tracked by the skipped test above. The underscore placeholder
// logic is implemented, but we can't verify it works until the broader
// multi-parameter function bug is fixed.
const code = `divide = fn a b: a / b end; result = 10 | divide 2 _; result`
test('pipe with named underscore arg', () => {
expect(`
divide = fn a b: a / b end
get-ten = fn: 10 end
get-ten | divide 2 b=_`).toEvaluateTo(0.2)
// Underscore is replaced with piped value (10)
// Should call: divide(2, 10) = 2 / 10 = 0.2
expect(code).toEvaluateTo(0.2)
expect(`
divide = fn a b: a / b end
get-ten = fn: 10 end
get-ten | divide b=_ 2`).toEvaluateTo(0.2)
expect(`
divide = fn a b: a / b end
get-ten = fn: 10 end
get-ten | divide 2 a=_`).toEvaluateTo(5)
expect(`
divide = fn a b: a / b end
get-ten = fn: 10 end
get-ten | divide a=_ 2`).toEvaluateTo(5)
})
test('pipe with underscore placeholder (single param verification)', () => {
// Since multi-param functions don't work yet, let's verify the underscore
// detection and replacement logic works by testing that underscore is NOT
// treated as a literal word/string
const code = `identity = fn x: x end; result = 42 | identity _; result`
// If underscore is properly detected and replaced, we get 42
// If it's treated as a Word, we'd get the string "_"
expect(code).toEvaluateTo(42)
test('nested pipes', () => {
// This is complicated, but the idea is to make sure the underscore
// handling logic works correctly when there are multiple pipe stages
// in a single expression.
expect(`
sub = fn a b: a - b end
div = fn a b: a / b end
sub 3 1 | div (sub 110 9 | sub 1) _ | div 5`).toEvaluateTo(10)
})
})

View File

@ -2,13 +2,12 @@ import { CompilerError } from '#compiler/compilerError.ts'
import * as terms from '#parser/shrimp.terms'
import type { SyntaxNode, Tree } from '@lezer/common'
export const checkTreeForErrors = (tree: Tree, input: string): string[] => {
const errors: string[] = []
export const checkTreeForErrors = (tree: Tree): CompilerError[] => {
const errors: CompilerError[] = []
tree.iterate({
enter: (node) => {
if (node.type.isError) {
const errorText = input.slice(node.from, node.to)
errors.push(`Syntax error at ${node.from}-${node.to}: "${errorText}"`)
errors.push(new CompilerError(`Unexpected syntax.`, node.from, node.to))
}
},
})
@ -70,18 +69,16 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
)
}
const paramNames = getAllChildren(paramsNode)
.map((param) => {
if (param.type.id !== terms.Identifier) {
throw new CompilerError(
`FunctionDef params must be Identifiers, got ${param.type.name}`,
param.from,
param.to
)
}
return input.slice(param.from, param.to)
})
.join(' ')
const paramNames = getAllChildren(paramsNode).map((param) => {
if (param.type.id !== terms.Identifier) {
throw new CompilerError(
`FunctionDef params must be Identifiers, got ${param.type.name}`,
param.from,
param.to
)
}
return input.slice(param.from, param.to)
})
return { paramNames, bodyNode }
}
@ -115,7 +112,7 @@ export const getNamedArgParts = (node: SyntaxNode, input: string) => {
throw new CompilerError(message, node.from, node.to)
}
const name = input.slice(namedArgPrefix.from, namedArgPrefix.to - 2) // Remove the trailing =
const name = input.slice(namedArgPrefix.from, namedArgPrefix.to - 1) // Remove the trailing =
return { name, valueNode }
}
@ -156,3 +153,15 @@ export const getIfExprParts = (node: SyntaxNode, input: string) => {
return { conditionNode, thenBlock, elseThenBlock, elseIfBlocks }
}
export const getPipeExprParts = (node: SyntaxNode) => {
const [pipedFunctionCall, operator, ...rest] = getAllChildren(node)
if (!pipedFunctionCall || !operator || rest.length === 0) {
const message = `PipeExpr expected at least 3 children, got ${getAllChildren(node).length}`
throw new CompilerError(message, node.from, node.to)
}
const pipeReceivers = rest.filter((child) => child.name !== 'operator')
return { pipedFunctionCall, pipeReceivers }
}

View File

@ -18,6 +18,7 @@
rightParen { ")" }
colon[closedBy="end", @name="colon"] { ":" }
end[openedBy="colon", @name="end"] { "end" }
Underscore { "_" }
"fn" [@name=keyword]
"if" [@name=keyword]
"elsif" [@name=keyword]
@ -68,7 +69,7 @@ PipeExpr {
}
pipeOperand {
FunctionCall | FunctionCallOrIdentifier | expressionWithoutIdentifier
FunctionCall | FunctionCallOrIdentifier
}
FunctionCallOrIdentifier {
@ -87,12 +88,13 @@ arg {
PositionalArg | NamedArg
}
PositionalArg {
expression | FunctionDef
expression | FunctionDef | Underscore
}
NamedArg {
NamedArgPrefix (expression | FunctionDef)
NamedArgPrefix (expression | FunctionDef | Underscore)
}
FunctionDef {
@ -158,7 +160,7 @@ BinOp {
}
ParenExpr {
leftParen (ambiguousFunctionCall | BinOp | expressionWithoutIdentifier | ConditionalOp ) rightParen
leftParen (ambiguousFunctionCall | BinOp | expressionWithoutIdentifier | ConditionalOp | PipeExpr) rightParen
}
expression {

View File

@ -17,10 +17,11 @@ export const
Params = 28,
colon = 29,
end = 30,
NamedArg = 31,
NamedArgPrefix = 32,
IfExpr = 34,
ThenBlock = 37,
ElsifExpr = 38,
ElseExpr = 40,
Assign = 42
Underscore = 31,
NamedArg = 32,
NamedArgPrefix = 33,
IfExpr = 35,
ThenBlock = 38,
ElsifExpr = 39,
ElseExpr = 41,
Assign = 43

View File

@ -4,10 +4,10 @@ import {tokenizer} from "./tokenizer"
import {highlighting} from "./highlight"
export const parser = LRParser.deserialize({
version: 14,
states: ",xQVQTOOO!lQUO'#CdO#PQPO'#DiO#_QPO'#CeO#mQPO'#DcO$gQTO'#CcOOQS'#Dg'#DgO$nQPO'#DfO%YQTO'#DkOOQS'#Cv'#CvO%bQPO'#C`O%gQTO'#DoOOQO'#DO'#DOOOQO'#Dc'#DcO%nQPO'#DbOOQS'#Db'#DbOOQS'#DX'#DXQVQTOOOOQS'#Df'#DfOOQS'#Cb'#CbO%vQTO'#C{OOQS'#De'#DeOOQS'#DY'#DYO&QQUO,58{O&nQTO,59rO%gQTO,59PO%gQTO,59PO&{QUO'#CdOOQO'#Di'#DiO(WQPO'#CeO(hQPO,58}O(tQPO,58}O(yQPO,58}OOQS'#DZ'#DZO)tQTO'#CxO)|QPO,5:VO*RQTO'#D]O*YQPO,58zO*hQPO,5:ZO*oQPO,5:ZOOQS,59|,59|OOQS-E7V-E7VOOQS,59g,59gOOQS-E7W-E7WOOQO1G/^1G/^OOQO1G.k1G.kO*tQPO1G.kO%gQTO,59UO%gQTO,59UOOQS1G.i1G.iOOQS-E7X-E7XO+`QTO1G/qO+pQUO'#CdOOQO'#Dd'#DdOOQO,59w,59wOOQO-E7Z-E7ZO,ZQTO1G/uOOQO1G.p1G.pO,kQPO1G.pO,uQPO7+%]O,zQTO7+%^OOQO'#DQ'#DQOOQO7+%a7+%aO-[QTO7+%bOOQS<<Hw<<HwO-rQPO'#D[O-wQTO'#DnO._QPO<<HxOOQO'#DR'#DRO.dQPO<<H|OOQS,59v,59vOOQS-E7Y-E7YOOQSAN>dAN>dO%gQTO'#DSOOQO'#D^'#D^O.oQPOAN>hO.zQPO'#DUOOQOAN>hAN>hO/PQPOAN>hO/UQPO,59nO/]QPO,59nOOQO-E7[-E7[OOQOG24SG24SO/bQPOG24SO/gQPO,59pO/lQPO1G/YOOQOLD)nLD)nO,zQTO1G/[O-[QTO7+$tOOQO7+$v7+$vOOQO<<H`<<H`",
stateData: "/t~O!TOS~OPPOQUOgUOhUOiUOkWOsZO![TO!a_O~OPbOQUOgUOhUOiUOkWOpdO![TOY!YXZ!YX[!YX]!YX~O_hOqWX!aWX!eWXnWX~PtOq!WX!a!]X!e!]Xn!]X~OYiOZiO[jO]jO~OYiOZiO[jO]jO!a!VX!e!VXn!VX~OQUOgUOhUOiUO![TO~OPkO~P$UOY!YXZ!YX[!YX]!YXq!WX!a!VX!e!VXn!VX~OPqOmlP~OqtO~OPbO~P$UO!axO!exO~OPbOkWO~P$UOPbOkWOpdOqTa!aTa!eTa!^TanTa~P$UOPPOkWOsZO~P$UO_!YX`!YXa!YXb!YXc!YXd!YXe!YXf!YX!^WX~PtO_!PO`!POa!POb!POc!POd!POe!QOf!QO~OYiOZiO[jO]jO~P'lOYiOZiO[jO]jO!^!RO~O!^!ROY!YXZ!YX[!YX]!YX_!YX`!YXa!YXb!YXc!YXd!YXe!YXf!YX~OPqOmlX~Om!TO~OP!UO~P$UOqtO!aSa!eSanSa~Om!YO~P'lOm!YO~OYiOZiO[Xi]Xi!aXi!eXi!^XinXi~OPPOkWOsZO!a!^O~P$UOPbOkWOpdOqWX!aWX!eWXnWX~P$UOPPOkWOsZO!a!aO~P$UO!^^im^i~P'lOn!bO~OPPOkWOsZOn!bP~P$UOPPOkWOsZOn!bPw!bPy!bP~P$UO!a!hO~OPPOkWOsZOn!bXw!bXy!bX~P$UOn!jO~On!oOw!kOy!nO~On!tOw!kOy!nO~Om!vO~On!tO~Om!wO~P'lOm!wO~On!xO~O!a!yO~O!a!zO~Oh]~",
goto: "*T!ePPPP!f!r#U#[!r#uPPPP$[PPPPPPPPPPP$hP$}PP#UPP!fP%Q%T%^P%bP!f%h%n%v%|&V&]PPP&c&g&{'['b(^P(}P)^)^P)o)w)we]Oah!T!Y!^!a!d!y!zdQOah!T!Y!^!a!d!y!zQlTR!VtXePgk!U!PUOPTZadghijkt!P!Q!T!U!Y!^!a!d!k!y!zdSOah!T!Y!^!a!d!y!zQnTQ}iR!OjQoTQwZQ!Z!QR!r!kd]Oah!T!Y!^!a!d!y!zWcPgk!URzdRsWR!`!YQ!g!aQ!{!yR!|!zT!l!g!mQ!p!gR!u!mQaORyaUgPk!UR{gQrWR!SrW!d!^!a!y!zR!i!dQuYR!XuQ!m!gR!s!mT`OaS^OaQ|hQ!]!TQ!_!YZ!c!^!a!d!y!zdYOah!T!Y!^!a!d!y!zR!WtXfPgk!UdROah!T!Y!^!a!d!y!zWcPgk!UQmTQvZQzdQ}iQ!OjQ!Z!PQ![!QR!q!kdVOah!T!Y!^!a!d!y!zfbPZdgijk!P!Q!U!kQpTR!Vtd]Oah!T!Y!^!a!d!y!zRoToXOPadghk!T!U!Y!^!a!d!y!zQ!e!^V!f!a!y!ze[Oah!T!Y!^!a!d!y!z",
nodeNames: "⚠ Identifier Word Program PipeExpr FunctionCall PositionalArg ParenExpr FunctionCallOrIdentifier BinOp operator operator operator operator ConditionalOp operator operator operator operator operator operator operator operator String Number Boolean FunctionDef keyword Params colon end NamedArg NamedArgPrefix operator IfExpr keyword ThenBlock ThenBlock ElsifExpr keyword ElseExpr keyword Assign",
states: ",rQVQTOOO!rQUO'#CdO#SQPO'#CeO#bQPO'#DdO$[QTO'#CcOOQS'#Dh'#DhO$cQPO'#DgO$zQTO'#DkOOQS'#Cv'#CvOOQO'#De'#DeO%SQPO'#DdO%bQTO'#DoOOQO'#DP'#DPOOQO'#Dd'#DdO%iQPO'#DcOOQS'#Dc'#DcOOQS'#DY'#DYQVQTOOOOQS'#Dg'#DgOOQS'#Cb'#CbO%qQTO'#C|OOQS'#Df'#DfOOQS'#DZ'#DZO&OQUO,58{O&oQTO,59sO%bQTO,59PO%bQTO,59PO&|QUO'#CdO(XQPO'#CeO(iQPO,58}O(zQPO,58}O(uQPO,58}O)uQPO,58}OOQS'#D['#D[O)}QTO'#CxO*VQPO,5:VO*[QTO'#D^O*aQPO,58zO*rQPO,5:ZO*yQPO,5:ZOOQS,59},59}OOQS-E7W-E7WOOQS,59h,59hOOQS-E7X-E7XOOQO1G/_1G/_OOQO1G.k1G.kO+OQPO1G.kO%bQTO,59UO%bQTO,59UOOQS1G.i1G.iOOQS-E7Y-E7YO+jQTO1G/qO+zQUO'#CdOOQO,59x,59xOOQO-E7[-E7[O,kQTO1G/uOOQO1G.p1G.pO,{QPO1G.pO-VQPO7+%]O-[QTO7+%^OOQO'#DR'#DROOQO7+%a7+%aO-lQTO7+%bOOQS<<Hw<<HwO.SQPO'#D]O.XQTO'#DnO.oQPO<<HxOOQO'#DS'#DSO.tQPO<<H|OOQS,59w,59wOOQS-E7Z-E7ZOOQSAN>dAN>dO%bQTO'#DTOOQO'#D_'#D_O/PQPOAN>hO/[QPO'#DVOOQOAN>hAN>hO/aQPOAN>hO/fQPO,59oO/mQPO,59oOOQO-E7]-E7]OOQOG24SG24SO/rQPOG24SO/wQPO,59qO/|QPO1G/ZOOQOLD)nLD)nO-[QTO1G/]O-lQTO7+$uOOQO7+$w7+$wOOQO<<Ha<<Ha",
stateData: "0U~O!UOS~OPPOQTOgTOhTOiTOkVOtZO!]SO!a_O~OPbOQTOgTOhTOiTOkVOocOqdO!]SOY!ZXZ!ZX[!ZX]!ZXrWX~O_hO!aWX!eWXnWX~PtOYiOZiO[jO]jO~OYiOZiO[jO]jO!a!WX!e!WXn!WX~OQTOgTOhTOiTO!]SO~OPkO~P#yOY!ZXZ!ZX[!ZX]!ZX!a!WX!e!WXn!WX~OPqOmlP~OrtO!a!WX!e!WXn!WX~OPbO~P#yO!axO!exO~OPbOkVOozO~P#yOPbOkVOocOqdOrTa!aTa!eTa!^TanTa~P#yOPPOkVOtZO~P#yO_!ZX`!ZXa!ZXb!ZXc!ZXd!ZXe!ZXf!ZX!^WX~PtO_!PO`!POa!POb!POc!POd!POe!QOf!QO~OYiOZiO[jO]jO~P'mOYiOZiO[jO]jO!^!RO~O!^!ROY!ZXZ!ZX[!ZX]!ZX_!ZX`!ZXa!ZXb!ZXc!ZXd!ZXe!ZXf!ZX~OrtO!^!RO~OPqOmlX~Om!TO~OP!UO~OrtO!aSa!eSa!^SanSa~Om!XO~P'mOm!XO~OYiOZiO[Xi]Xi!aXi!eXi!^XinXi~OPPOkVOtZO!a!]O~P#yOPbOkVOocOqdOrWX!aWX!eWX!^WXnWX~P#yOPPOkVOtZO!a!`O~P#yO!^^im^i~P'mOn!aO~OPPOkVOtZOn!bP~P#yOPPOkVOtZOn!bPx!bPz!bP~P#yO!a!gO~OPPOkVOtZOn!bXx!bXz!bX~P#yOn!iO~On!nOx!jOz!mO~On!sOx!jOz!mO~Om!uO~On!sO~Om!vO~P'mOm!vO~On!wO~O!a!xO~O!a!yO~Oh]~",
goto: "*P!ePPPP!f!u#T#Z!u#sPPPP$YPPPPPPPPPPP$fP${PPP#TPP%OP%[%_%hP%lP%O%r%x&Q&W&a&hPPP&n&r'W'j'p(lPP)Y)YP)k)s)sd]Oah!T!X!]!`!c!x!yRoSiXOSaht!T!X!]!`!c!x!yXePgk!U}TOPSZadghijk!P!Q!T!U!X!]!`!c!j!x!ydROah!T!X!]!`!c!x!yQmSQ}iR!OjQoSQwZQ!Y!QR!q!jd]Oah!T!X!]!`!c!x!yWcPgk!URzdRsVe]Oah!T!X!]!`!c!x!yR!_!XQ!f!`Q!z!xR!{!yT!k!f!lQ!o!fR!t!lQaORyaUgPk!UR{gQrVR!SrW!c!]!`!x!yR!h!cSuYpR!WuQ!l!fR!r!lT`OaS^OaQ|hQ![!TQ!^!XZ!b!]!`!c!x!ydYOah!T!X!]!`!c!x!yQpSR!VtXfPgk!UdQOah!T!X!]!`!c!x!yWcPgk!UQlSQvZQzdQ}iQ!OjQ!Y!PQ!Z!QR!p!jdUOah!T!X!]!`!c!x!yfbPZdgijk!P!Q!U!jRnSoWOPadghk!T!U!X!]!`!c!x!yQ!d!]V!e!`!x!ye[Oah!T!X!]!`!c!x!y",
nodeNames: "⚠ Identifier Word Program PipeExpr FunctionCall PositionalArg ParenExpr FunctionCallOrIdentifier BinOp operator operator operator operator ConditionalOp operator operator operator operator operator operator operator operator String Number Boolean FunctionDef keyword Params colon end Underscore NamedArg NamedArgPrefix operator IfExpr keyword ThenBlock ThenBlock ElsifExpr keyword ElseExpr keyword Assign",
maxTerm: 67,
nodeProps: [
["closedBy", 29,"end"],
@ -16,8 +16,8 @@ export const parser = LRParser.deserialize({
propSources: [highlighting],
skippedNodes: [0],
repeatNodeCount: 6,
tokenData: "-u~RnXY#PYZ#Upq#Pqr#Zwx#fxy$Tyz$Yz{$_{|$d}!O$i!P!Q%[!Q![$q![!]%a!]!^#U!^!_%f!_!`%s!`!a%x#T#U&V#U#X&k#X#Y'`#Y#Z)|#Z#]&k#]#^+u#^#c&k#c#d,a#d#h&k#h#i,{#i#o&k#p#q-k~~-p~#UO!T~~#ZO!a~~#^P!_!`#a~#fO`~~#iTOw#fwx#xx;'S#f;'S;=`#}<%lO#f~#}Og~~$QP;=`<%l#f~$YO![~~$_O!^~~$dOY~~$iO[~~$nP]~!Q![$q~$vQh~!O!P$|!Q![$q~%PP!Q![%S~%XPh~!Q![%S~%aOZ~~%fOm~~%kPa~!_!`%n~%sOb~~%xO_~~%}Pc~!_!`&Q~&VOd~~&YS!_!`&f#T#b&k#b#c&t#c#o&kQ&kOpQQ&nQ!_!`&f#T#o&k~&wS!_!`&f#T#W&k#W#X'T#X#o&k~'YQe~!_!`&f#T#o&k~'cU!_!`&f#T#`&k#`#a'u#a#b&k#b#c)b#c#o&kR'xS!_!`&f#T#g&k#g#h(U#h#o&kR(XU!_!`&f#T#X&k#X#Y(k#Y#]&k#]#^(v#^#o&kR(pQyP!_!`&f#T#o&kR(yS!_!`&f#T#Y&k#Y#Z)V#Z#o&kR)[QwP!_!`&f#T#o&k~)eS!_!`&f#T#W&k#W#X)q#X#o&k~)vQn~!_!`&f#T#o&k~*PT!_!`&f#T#U*`#U#b&k#b#c+j#c#o&k~*cS!_!`&f#T#`&k#`#a*o#a#o&k~*rS!_!`&f#T#g&k#g#h+O#h#o&k~+RS!_!`&f#T#X&k#X#Y+_#Y#o&k~+dQi~!_!`&f#T#o&k~+oQk~!_!`&f#T#o&kR+xS!_!`&f#T#Y&k#Y#Z,U#Z#o&kR,ZQsP!_!`&f#T#o&k~,dS!_!`&f#T#f&k#f#g,p#g#o&k~,uQf~!_!`&f#T#o&k~-OS!_!`&f#T#f&k#f#g-[#g#o&k~-_S!_!`&f#T#i&k#i#j+O#j#o&k~-pOq~~-uO!e~",
tokenData: "-}~RoXY#SYZ#Xpq#Sqr#^wx#ixy$Wyz$]z{$b{|$g}!O$l!P!Q%_!Q![$t![!]%d!]!^#X!^!_%i!_!`%v!`!a%{#R#S&Y#T#U&_#U#X&s#X#Y'h#Y#Z*U#Z#]&s#]#^+}#^#c&s#c#d,i#d#h&s#h#i-T#i#o&s#p#q-s~~-x~#XO!U~~#^O!a~~#aP!_!`#d~#iO`~~#lTOw#iwx#{x;'S#i;'S;=`$Q<%lO#i~$QOg~~$TP;=`<%l#i~$]O!]~~$bO!^~~$gOY~~$lO[~~$qP]~!Q![$t~$yQh~!O!P%P!Q![$t~%SP!Q![%V~%[Ph~!Q![%V~%dOZ~~%iOm~~%nPa~!_!`%q~%vOb~~%{O_~~&QPc~!_!`&T~&YOd~~&_Oo~~&bS!_!`&n#T#b&s#b#c&|#c#o&sQ&sOqQQ&vQ!_!`&n#T#o&s~'PS!_!`&n#T#W&s#W#X']#X#o&s~'bQe~!_!`&n#T#o&s~'kU!_!`&n#T#`&s#`#a'}#a#b&s#b#c)j#c#o&sR(QS!_!`&n#T#g&s#g#h(^#h#o&sR(aU!_!`&n#T#X&s#X#Y(s#Y#]&s#]#^)O#^#o&sR(xQzP!_!`&n#T#o&sR)RS!_!`&n#T#Y&s#Y#Z)_#Z#o&sR)dQxP!_!`&n#T#o&s~)mS!_!`&n#T#W&s#W#X)y#X#o&s~*OQn~!_!`&n#T#o&s~*XT!_!`&n#T#U*h#U#b&s#b#c+r#c#o&s~*kS!_!`&n#T#`&s#`#a*w#a#o&s~*zS!_!`&n#T#g&s#g#h+W#h#o&s~+ZS!_!`&n#T#X&s#X#Y+g#Y#o&s~+lQi~!_!`&n#T#o&s~+wQk~!_!`&n#T#o&sR,QS!_!`&n#T#Y&s#Y#Z,^#Z#o&sR,cQtP!_!`&n#T#o&s~,lS!_!`&n#T#f&s#f#g,x#g#o&s~,}Qf~!_!`&n#T#o&s~-WS!_!`&n#T#f&s#f#g-d#g#o&s~-gS!_!`&n#T#i&s#i#j+W#j#o&s~-xOr~~-}O!e~",
tokenizers: [0, 1, tokenizer],
topRules: {"Program":[0,3]},
tokenPrec: 677
tokenPrec: 693
})

View File

@ -23,11 +23,6 @@ export const tokenizer = new ExternalTokenizer((input: InputStream, stack: Stack
}
}
// Pipe character always terminates a word/identifier
if (ch === 124 /* | */) {
break
}
// Track identifier validity
if (!isLowercaseLetter(ch) && !isDigit(ch) && ch !== 45 && !isEmoji(ch)) {
if (!canBeWord) break