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) 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', () => { test('function call with no args', () => {
expect(`bloop = fn: 'bloop' end; bloop`).toEvaluateTo('bloop') expect(`bloop = fn: 'bloop' end; bloop`).toEvaluateTo('bloop')
}) })
@ -136,7 +147,7 @@ describe('errors', () => {
}) })
}) })
describe.skip('multiline tests', () => { describe('multiline tests', () => {
test('multiline function', () => { test('multiline function', () => {
expect(` expect(`
add = fn a b: add = fn a b:

View File

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

View File

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

View File

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

View File

@ -2,13 +2,12 @@ import { CompilerError } from '#compiler/compilerError.ts'
import * as terms from '#parser/shrimp.terms' import * as terms from '#parser/shrimp.terms'
import type { SyntaxNode, Tree } from '@lezer/common' import type { SyntaxNode, Tree } from '@lezer/common'
export const checkTreeForErrors = (tree: Tree, input: string): string[] => { export const checkTreeForErrors = (tree: Tree): CompilerError[] => {
const errors: string[] = [] const errors: CompilerError[] = []
tree.iterate({ tree.iterate({
enter: (node) => { enter: (node) => {
if (node.type.isError) { if (node.type.isError) {
const errorText = input.slice(node.from, node.to) errors.push(new CompilerError(`Unexpected syntax.`, node.from, node.to))
errors.push(`Syntax error at ${node.from}-${node.to}: "${errorText}"`)
} }
}, },
}) })
@ -70,8 +69,7 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
) )
} }
const paramNames = getAllChildren(paramsNode) const paramNames = getAllChildren(paramsNode).map((param) => {
.map((param) => {
if (param.type.id !== terms.Identifier) { if (param.type.id !== terms.Identifier) {
throw new CompilerError( throw new CompilerError(
`FunctionDef params must be Identifiers, got ${param.type.name}`, `FunctionDef params must be Identifiers, got ${param.type.name}`,
@ -81,7 +79,6 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
} }
return input.slice(param.from, param.to) return input.slice(param.from, param.to)
}) })
.join(' ')
return { paramNames, bodyNode } return { paramNames, bodyNode }
} }
@ -115,7 +112,7 @@ export const getNamedArgParts = (node: SyntaxNode, input: string) => {
throw new CompilerError(message, node.from, node.to) 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 } return { name, valueNode }
} }
@ -156,3 +153,15 @@ export const getIfExprParts = (node: SyntaxNode, input: string) => {
return { conditionNode, thenBlock, elseThenBlock, elseIfBlocks } 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 { ")" } rightParen { ")" }
colon[closedBy="end", @name="colon"] { ":" } colon[closedBy="end", @name="colon"] { ":" }
end[openedBy="colon", @name="end"] { "end" } end[openedBy="colon", @name="end"] { "end" }
Underscore { "_" }
"fn" [@name=keyword] "fn" [@name=keyword]
"if" [@name=keyword] "if" [@name=keyword]
"elsif" [@name=keyword] "elsif" [@name=keyword]
@ -68,7 +69,7 @@ PipeExpr {
} }
pipeOperand { pipeOperand {
FunctionCall | FunctionCallOrIdentifier | expressionWithoutIdentifier FunctionCall | FunctionCallOrIdentifier
} }
FunctionCallOrIdentifier { FunctionCallOrIdentifier {
@ -87,12 +88,13 @@ arg {
PositionalArg | NamedArg PositionalArg | NamedArg
} }
PositionalArg { PositionalArg {
expression | FunctionDef expression | FunctionDef | Underscore
} }
NamedArg { NamedArg {
NamedArgPrefix (expression | FunctionDef) NamedArgPrefix (expression | FunctionDef | Underscore)
} }
FunctionDef { FunctionDef {
@ -158,7 +160,7 @@ BinOp {
} }
ParenExpr { ParenExpr {
leftParen (ambiguousFunctionCall | BinOp | expressionWithoutIdentifier | ConditionalOp ) rightParen leftParen (ambiguousFunctionCall | BinOp | expressionWithoutIdentifier | ConditionalOp | PipeExpr) rightParen
} }
expression { expression {

View File

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

View File

@ -4,10 +4,10 @@ import {tokenizer} from "./tokenizer"
import {highlighting} from "./highlight" import {highlighting} from "./highlight"
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, 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`", 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: "/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]~", 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: "*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", 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 NamedArg NamedArgPrefix operator IfExpr keyword ThenBlock ThenBlock ElsifExpr keyword ElseExpr keyword Assign", 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, maxTerm: 67,
nodeProps: [ nodeProps: [
["closedBy", 29,"end"], ["closedBy", 29,"end"],
@ -16,8 +16,8 @@ export const parser = LRParser.deserialize({
propSources: [highlighting], propSources: [highlighting],
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 6, 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], tokenizers: [0, 1, tokenizer],
topRules: {"Program":[0,3]}, 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 // Track identifier validity
if (!isLowercaseLetter(ch) && !isDigit(ch) && ch !== 45 && !isEmoji(ch)) { if (!isLowercaseLetter(ch) && !isDigit(ch) && ch !== 45 && !isEmoji(ch)) {
if (!canBeWord) break if (!canBeWord) break