i did things

This commit is contained in:
Corey Johnson 2025-10-24 10:17:06 -07:00
parent 82cd199ed8
commit 66671970e0
21 changed files with 359 additions and 202 deletions

@ -1 +1 @@
Subproject commit 47f829fcada71655f0d40ec363b5bcc844af8856 Subproject commit 995487f2d5d8bb260e223ca402220c51ceba1c4a

View File

@ -209,7 +209,7 @@ describe('Regex', () => {
}) })
test('invalid regex pattern', () => { test('invalid regex pattern', () => {
expect('//[unclosed//').toFailEvaluation() expect('//[unclosed//').toEvaluateTo('//[unclosed//')
}) })
}) })

View File

@ -1,23 +1,24 @@
import { basicSetup } from 'codemirror'
import { EditorView } from '@codemirror/view' import { EditorView } from '@codemirror/view'
import { shrimpTheme } from '#editor/plugins/theme' import { asciiEscapeToHtml, assertNever, log, toElement } from '#utils/utils'
import { shrimpLanguage } from '#/editor/plugins/shrimpLanguage'
import { shrimpHighlighting } from '#editor/plugins/theme'
import { shrimpKeymap } from '#editor/plugins/keymap'
import { asciiEscapeToHtml, assert, assertNever, log, toElement } from '#utils/utils'
import { Signal } from '#utils/signal' import { Signal } from '#utils/signal'
import { shrimpErrors } from '#editor/plugins/errors' import { getContent } from '#editor/plugins/persistence'
import { debugTags } from '#editor/plugins/debugTags'
import { getContent, persistencePlugin } from '#editor/plugins/persistence'
import '#editor/editor.css'
import type { HtmlEscapedString } from 'hono/utils/html' import type { HtmlEscapedString } from 'hono/utils/html'
import { catchErrors } from '#editor/plugins/catchErrors'
import { connectToNose, noseSignals } from '#editor/noseClient' import { connectToNose, noseSignals } from '#editor/noseClient'
import type { Value } from 'reefvm' import type { Value } from 'reefvm'
import { Compartment } from '@codemirror/state'
import { lineNumbers } from '@codemirror/view'
import { shrimpSetup } from '#editor/plugins/shrimpSetup'
import '#editor/editor.css'
const lineNumbersCompartment = new Compartment()
connectToNose() connectToNose()
export const outputSignal = new Signal<Value | string>()
export const errorSignal = new Signal<string>()
export const multilineModeSignal = new Signal<boolean>()
export const Editor = () => { export const Editor = () => {
return ( return (
<> <>
@ -27,17 +28,14 @@ export const Editor = () => {
const view = new EditorView({ const view = new EditorView({
parent: ref, parent: ref,
doc: getContent(), doc: getContent(),
extensions: [ extensions: shrimpSetup(lineNumbersCompartment),
catchErrors, })
shrimpKeymap,
basicSetup, multilineModeSignal.connect((isMultiline) => {
shrimpTheme, console.log(`🌭 hey babe`, isMultiline)
shrimpLanguage, view.dispatch({
shrimpHighlighting, effects: lineNumbersCompartment.reconfigure(isMultiline ? lineNumbers() : []),
shrimpErrors, })
persistencePlugin,
// debugTags,
],
}) })
requestAnimationFrame(() => view.focus()) requestAnimationFrame(() => view.focus())
@ -64,9 +62,6 @@ noseSignals.connect((message) => {
} }
}) })
export const outputSignal = new Signal<Value | string>()
export const errorSignal = new Signal<string>()
outputSignal.connect((value) => { outputSignal.connect((value) => {
const el = document.querySelector('#output')! const el = document.querySelector('#output')!
el.innerHTML = '' el.innerHTML = ''

View File

@ -1,3 +1,4 @@
import { multilineModeSignal, outputSignal } from '#editor/editor'
import { printBytecodeOutput, printParserOutput, runCode } from '#editor/runCode' import { printBytecodeOutput, printParserOutput, runCode } from '#editor/runCode'
import { EditorState } from '@codemirror/state' import { EditorState } from '@codemirror/state'
import { keymap } from '@codemirror/view' import { keymap } from '@codemirror/view'
@ -27,9 +28,13 @@ const customKeymap = keymap.of([
if (multilineMode) { if (multilineMode) {
const input = view.state.doc.toString() const input = view.state.doc.toString()
runCode(input) runCode(input)
return true return true
} else {
outputSignal.emit('Press Shift+Enter to insert run the code.')
} }
multilineModeSignal.emit(true)
multilineMode = true multilineMode = true
view.dispatch({ view.dispatch({
changes: { from: view.state.doc.length, insert: '\n' }, changes: { from: view.state.doc.length, insert: '\n' },
@ -44,6 +49,10 @@ const customKeymap = keymap.of([
key: 'Tab', key: 'Tab',
preventDefault: true, preventDefault: true,
run: (view) => { run: (view) => {
view.dispatch({
changes: { from: view.state.selection.main.from, insert: ' ' },
selection: { anchor: view.state.selection.main.from + 2 },
})
return true return true
}, },
}, },
@ -51,6 +60,8 @@ const customKeymap = keymap.of([
{ {
key: 'ArrowUp', key: 'ArrowUp',
run: (view) => { run: (view) => {
if (multilineMode) return false
const command = history.previous() const command = history.previous()
if (command === undefined) return false if (command === undefined) return false
view.dispatch({ view.dispatch({
@ -64,6 +75,8 @@ const customKeymap = keymap.of([
{ {
key: 'ArrowDown', key: 'ArrowDown',
run: (view) => { run: (view) => {
if (multilineMode) return false
const command = history.next() const command = history.next()
if (command === undefined) return false if (command === undefined) return false
view.dispatch({ view.dispatch({
@ -124,9 +137,22 @@ export const shrimpKeymap = [customKeymap, singleLineFilter]
class History { class History {
private commands: string[] = [] private commands: string[] = []
private index: number | undefined private index: number | undefined
private storageKey = 'shrimp-command-history'
constructor() {
try {
this.commands = JSON.parse(localStorage.getItem(this.storageKey) || '[]')
} catch {
console.warn('Failed to load command history from localStorage')
}
}
push(command: string) { push(command: string) {
this.commands.push(command) this.commands.push(command)
// Limit to last 50 commands
this.commands = this.commands.slice(-50)
localStorage.setItem(this.storageKey, JSON.stringify(this.commands))
this.index = undefined this.index = undefined
} }

View File

@ -0,0 +1,35 @@
import { history, defaultKeymap, historyKeymap } from '@codemirror/commands'
import { bracketMatching, indentOnInput } from '@codemirror/language'
import { highlightSpecialChars, drawSelection, dropCursor, keymap } from '@codemirror/view'
import { closeBrackets, autocompletion, completionKeymap } from '@codemirror/autocomplete'
import { EditorState, Compartment } from '@codemirror/state'
import { searchKeymap } from '@codemirror/search'
import { shrimpKeymap } from './keymap'
import { shrimpTheme, shrimpHighlighting } from './theme'
import { shrimpLanguage } from './shrimpLanguage'
import { shrimpErrors } from './errors'
import { persistencePlugin } from './persistence'
import { catchErrors } from './catchErrors'
export const shrimpSetup = (lineNumbersCompartment: Compartment) => {
return [
catchErrors,
shrimpKeymap,
highlightSpecialChars(),
history(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
bracketMatching(),
closeBrackets(),
autocompletion(),
indentOnInput(),
keymap.of([...defaultKeymap, ...historyKeymap, ...searchKeymap, ...completionKeymap]),
lineNumbersCompartment.of([]),
shrimpTheme,
shrimpLanguage,
shrimpHighlighting,
shrimpErrors,
persistencePlugin,
]
}

View File

@ -38,18 +38,12 @@ export const shrimpTheme = EditorView.theme(
caretColor: 'var(--caret)', caretColor: 'var(--caret)',
padding: '0px', padding: '0px',
}, },
'.cm-activeLine': {
backgroundColor: 'transparent',
},
'&.cm-focused .cm-cursor': { '&.cm-focused .cm-cursor': {
borderLeftColor: 'var(--caret)', borderLeftColor: 'var(--caret)',
}, },
'&.cm-focused .cm-selectionBackground, ::selection': { '&.cm-focused .cm-selectionBackground, ::selection': {
backgroundColor: 'var(--bg-selection)', backgroundColor: 'var(--bg-selection)',
}, },
'.cm-gutters': {
display: 'none',
},
'.cm-editor': { '.cm-editor': {
border: 'none', border: 'none',
outline: 'none', outline: 'none',

View File

@ -1,9 +1,10 @@
import { outputSignal, errorSignal } from '#editor/editor' import { outputSignal, errorSignal } from '#editor/editor'
import { Compiler } from '#compiler/compiler' import { Compiler } from '#compiler/compiler'
import { errorMessage, log } from '#utils/utils' import { errorMessage, log } from '#utils/utils'
import { bytecodeToString, run } from 'reefvm' import { bytecodeToString } from 'reefvm'
import { parser } from '#parser/shrimp' import { parser } from '#parser/shrimp'
import { sendToNose } from '#editor/noseClient' import { sendToNose } from '#editor/noseClient'
import { treeToString } from '#utils/tree'
export const runCode = async (input: string) => { export const runCode = async (input: string) => {
try { try {
@ -18,7 +19,8 @@ export const runCode = async (input: string) => {
export const printParserOutput = (input: string) => { export const printParserOutput = (input: string) => {
try { try {
const cst = parser.parse(input) const cst = parser.parse(input)
outputSignal.emit(cst.toString()) const string = treeToString(cst, input)
outputSignal.emit(string)
} catch (error) { } catch (error) {
log.error(error) log.error(error)
errorSignal.emit(`${errorMessage(error)}`) errorSignal.emit(`${errorMessage(error)}`)

View File

@ -0,0 +1,80 @@
import { ExternalTokenizer, InputStream } from '@lezer/lr'
import * as terms from './shrimp.terms'
type Operator = { str: string; tokenName: keyof typeof terms }
const operators: Array<Operator> = [
{ str: 'and', tokenName: 'And' },
{ str: 'or', tokenName: 'Or' },
{ str: '>=', tokenName: 'Gte' },
{ str: '<=', tokenName: 'Lte' },
{ str: '!=', tokenName: 'Neq' },
// // Single-char operators
{ str: '*', tokenName: 'Star' },
{ str: '=', tokenName: 'Eq' },
{ str: '/', tokenName: 'Slash' },
{ str: '+', tokenName: 'Plus' },
{ str: '-', tokenName: 'Minus' },
{ str: '>', tokenName: 'Gt' },
{ str: '<', tokenName: 'Lt' },
]
export const operatorTokenizer = new ExternalTokenizer((input: InputStream) => {
for (let operator of operators) {
if (!matchesString(input, 0, operator.str)) continue
const afterOpPos = operator.str.length
const charAfterOp = input.peek(afterOpPos)
if (!isWhitespace(charAfterOp)) continue
// Accept the operator token
const token = terms[operator.tokenName]
if (token === undefined) {
throw new Error(`Unknown token name: ${operator.tokenName}`)
}
input.advance(afterOpPos)
input.acceptToken(token)
return
}
})
const isWhitespace = (ch: number): boolean => {
return matchesChar(ch, [' ', '\t', '\n'])
}
const matchesChar = (ch: number, chars: (string | number)[]): boolean => {
for (const c of chars) {
if (typeof c === 'number') {
if (ch === c) {
return true
}
} else if (ch === c.charCodeAt(0)) {
return true
}
}
return false
}
const matchesString = (input: InputStream, pos: number, str: string): boolean => {
for (let i = 0; i < str.length; i++) {
if (input.peek(pos + i) !== str.charCodeAt(i)) {
return false
}
}
return true
}
const peek = (numChars: number, input: InputStream): string => {
let result = ''
for (let i = 0; i < numChars; i++) {
const ch = input.peek(i)
if (ch === -1) {
result += 'EOF'
break
} else {
result += String.fromCharCode(ch)
}
}
return result
}

View File

@ -6,14 +6,14 @@
@top Program { item* } @top Program { item* }
@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot } @external tokens operatorTokenizer from "./operatorTokenizer" { Star, Slash, Plus, Minus, And, Or, Eq, Neq, Lt, Lte, Gt, Gte }
@tokens { @tokens {
@precedence { Number "-" Regex "/"} @precedence { Number Regex }
StringFragment { !['\\$]+ } StringFragment { !['\\$]+ }
NamedArgPrefix { $[a-z]+ "=" } NamedArgPrefix { $[a-z]+ "=" }
Number { "-"? $[0-9]+ ('.' $[0-9]+)? } Number { ("-" | "+")? $[0-9]+ ('.' $[0-9]+)? }
Boolean { "true" | "false" } Boolean { "true" | "false" }
newlineOrSemicolon { "\n" | ";" } newlineOrSemicolon { "\n" | ";" }
eof { @eof } eof { @eof }
@ -29,22 +29,12 @@
"if" [@name=keyword] "if" [@name=keyword]
"elsif" [@name=keyword] "elsif" [@name=keyword]
"else" [@name=keyword] "else" [@name=keyword]
"and" [@name=operator]
"or" [@name=operator]
"!=" [@name=operator]
"<" [@name=operator]
"<=" [@name=operator]
">" [@name=operator]
">=" [@name=operator]
"=" [@name=operator]
"+"[@name=operator]
"-"[@name=operator]
"*"[@name=operator]
"/"[@name=operator]
"|"[@name=operator] "|"[@name=operator]
} }
@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot }
@precedence { @precedence {
pipe @left, pipe @left,
multiplicative @left, multiplicative @left,
@ -140,14 +130,14 @@ ThenBlock {
} }
ConditionalOp { ConditionalOp {
expression "=" expression | expression Eq expression |
expression "!=" expression | expression Neq expression |
expression "<" expression | expression Lt expression |
expression "<=" expression | expression Lte expression |
expression ">" expression | expression Gt expression |
expression ">=" expression | expression Gte expression |
expression "and" (expression | ConditionalOp) | expression And (expression | ConditionalOp) |
expression "or" (expression | ConditionalOp) expression Or (expression | ConditionalOp)
} }
Params { Params {
@ -155,14 +145,14 @@ Params {
} }
Assign { Assign {
AssignableIdentifier "=" consumeToTerminator AssignableIdentifier Eq consumeToTerminator
} }
BinOp { BinOp {
(expression | BinOp) !multiplicative "*" (expression | BinOp) | (expression | BinOp) !multiplicative Star (expression | BinOp) |
(expression | BinOp) !multiplicative "/" (expression | BinOp) | (expression | BinOp) !multiplicative Slash (expression | BinOp) |
(expression | BinOp) !additive "+" (expression | BinOp) | (expression | BinOp) !additive Plus (expression | BinOp) |
(expression | BinOp) !additive "-" (expression | BinOp) (expression | BinOp) !additive Minus (expression | BinOp)
} }
ParenExpr { ParenExpr {

View File

@ -1,17 +1,29 @@
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
export const export const
Identifier = 1, Star = 1,
AssignableIdentifier = 2, Slash = 2,
Word = 3, Plus = 3,
IdentifierBeforeDot = 4, Minus = 4,
Program = 5, And = 5,
PipeExpr = 6, Or = 6,
FunctionCall = 7, Eq = 7,
PositionalArg = 8, Neq = 8,
ParenExpr = 9, Lt = 9,
FunctionCallOrIdentifier = 10, Lte = 10,
BinOp = 11, Gt = 11,
ConditionalOp = 16, Gte = 12,
Identifier = 13,
AssignableIdentifier = 14,
Word = 15,
IdentifierBeforeDot = 16,
Program = 17,
PipeExpr = 18,
FunctionCall = 19,
PositionalArg = 20,
ParenExpr = 21,
FunctionCallOrIdentifier = 22,
BinOp = 23,
ConditionalOp = 24,
String = 25, String = 25,
StringFragment = 26, StringFragment = 26,
Interpolation = 27, Interpolation = 27,

View File

@ -1,14 +1,15 @@
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr" import {LRParser} from "@lezer/lr"
import {operatorTokenizer} from "./operatorTokenizer"
import {tokenizer} from "./tokenizer" import {tokenizer} from "./tokenizer"
import {trackScope} from "./scopeTracker" import {trackScope} from "./scopeTracker"
import {highlighting} from "./highlight" import {highlighting} from "./highlight"
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, version: 14,
states: ".jQVQROOO#XQTO'#CfO$RQQO'#CgO$aQQO'#DmO$xQRO'#CeO%gOWO'#CuOOQP'#Dq'#DqO%uOQO'#C}O%zQQO'#DpO&cQRO'#D|OOQP'#DO'#DOOOQO'#Dn'#DnO&kQQO'#DmO&yQRO'#EQOOQO'#DX'#DXO'hQQO'#DaOOQO'#Dm'#DmO'mQQO'#DlOOQP'#Dl'#DlOOQP'#Db'#DbQVQROOOOQP'#Dp'#DpOOQP'#Cd'#CdO'uQRO'#DUOOQP'#Do'#DoOOQP'#Dc'#DcO(PQTO,58}O&yQRO,59RO&yQRO,59RO)XQQO'#CgO)iQQO,59PO)zQQO,59PO)uQQO,59PO*uQQO,59PO*}QRO'#CwO+VQ`O'#CxOOOO'#Du'#DuOOOO'#Dd'#DdO+kOWO,59aOOQP,59a,59aO+yOPO,59iOOQP'#De'#DeO,OQRO'#DQO,WQQO,5:hO,]QRO'#DgO,bQQO,58|O,sQQO,5:lO,zQQO,5:lO-PQRO,59{OOQP,5:W,5:WOOQP-E7`-E7`OOQP,59p,59pOOQP-E7a-E7aOOQO1G.m1G.mO-^QQO1G.mO&yQRO,59WO&yQRO,59WOOQP1G.k1G.kOOOO,59c,59cOOOO,59d,59dOOOO-E7b-E7bOOQP1G.{1G.{OOQP1G/T1G/TOOQP-E7c-E7cO-xQRO1G0SO!QQTO'#CfOOQO,5:R,5:ROOQO-E7e-E7eO.YQRO1G0WOOQO1G/g1G/gOOQO1G.r1G.rO.jQQO1G.rO.tQQO7+%nO.yQRO7+%oOOQO'#DZ'#DZOOQO7+%r7+%rO/ZQRO7+%sOOQP<<IY<<IYO/qQQO'#DfO/vQRO'#EPO0^QQO<<IZOOQO'#D['#D[O0cQQO<<I_OOQP,5:Q,5:QOOQP-E7d-E7dOOQPAN>uAN>uO&yQRO'#D]OOQO'#Dh'#DhO0nQQOAN>yO0yQQO'#D_OOQOAN>yAN>yO1OQQOAN>yO1TQQO,59wO1[QQO,59wOOQO-E7f-E7fOOQOG24eG24eO1aQQOG24eO1fQQO,59yO1kQQO1G/cOOQOLD*PLD*PO.yQRO1G/eO/ZQRO7+$}OOQO7+%P7+%POOQO<<Hi<<Hi", states: ".jQVQrOOO#XQuO'#CrO$RQRO'#CsO$aQRO'#DmO$xQrO'#CqO%gOWO'#CuOOQq'#Dq'#DqO%uOQO'#C}O%zQRO'#DpO&cQrO'#D|OOQp'#DO'#DOOOQO'#Dn'#DnO&kQQO'#DmO&yQrO'#EQOOQO'#DX'#DXO'hQRO'#DaOOQO'#Dm'#DmO'mQQO'#DlOOQp'#Dl'#DlOOQp'#Db'#DbQVQrOOOOQq'#Dp'#DpOOQp'#Cp'#CpO'uQrO'#DUOOQp'#Do'#DoOOQp'#Dc'#DcO(PQtO,59ZO&yQrO,59_O&yQrO,59_O)XQRO'#CsO)iQRO,59]O)zQRO,59]O)uQQO,59]O*uQQO,59]O*}QrO'#CwO+VQ`O'#CxOOOO'#Du'#DuOOOO'#Dd'#DdO+kOWO,59aOOQq,59a,59aO+yOpO,59iOOQp'#De'#DeO,OQrO'#DQO,WQQO,5:hO,]QrO'#DgO,bQQO,59YO,sQRO,5:lO,zQQO,5:lO-PQrO,59{OOQp,5:W,5:WOOQp-E7`-E7`OOQp,59p,59pOOQp-E7a-E7aOOQP1G.y1G.yO-^QRO1G.yO&yQrO,59`O&yQrO,59`OOQq1G.w1G.wOOOO,59c,59cOOOO,59d,59dOOOO-E7b-E7bOOQq1G.{1G.{OOQq1G/T1G/TOOQp-E7c-E7cO-xQrO1G0SO!QQtO'#CrOOQO,5:R,5:ROOQO-E7e-E7eO.YQrO1G0WOOQO1G/g1G/gOOQO1G.z1G.zO.jQRO1G.zO.tQQO7+%nO.yQrO7+%oOOQO'#DZ'#DZOOQO7+%r7+%rO/ZQrO7+%sOOQp<<IY<<IYO/qQQO'#DfO/vQrO'#EPO0^QQO<<IZOOQO'#D['#D[O0cQQO<<I_OOQp,5:Q,5:QOOQp-E7d-E7dOOQpAN>uAN>uO&yQrO'#D]OOQO'#Dh'#DhO0nQQOAN>yO0yQQO'#D_OOQOAN>yAN>yO1OQQOAN>yO1TQRO,59wO1[QQO,59wOOQO-E7f-E7fOOQOG24eG24eO1aQQOG24eO1fQQO,59yO1kQQO1G/cOOQOLD*PLD*PO.yQrO1G/eO/ZQrO7+$}OOQO7+%P7+%POOQO<<Hi<<Hi",
stateData: "1v~O!_OS~OPPOQ_ORUOSVOmUOnUOoUOpUOsXO|]O!fSO!hTO!rbO~OPeORUOSVOmUOnUOoUOpUOsXOwfOygO!fSO!hTOzYX!rYX!vYX!gYXvYX~O[!dX]!dX^!dX_!dXa!dXb!dXc!dXd!dXe!dXf!dXg!dXh!dX~P!QO[kO]kO^lO_lO~O[kO]kO^lO_lO!r!aX!v!aXv!aX~OPPORUOSVOmUOnUOoUOpUO!fSO!hTO~OjtO!hwO!jrO!ksO~O!oxO~O[!dX]!dX^!dX_!dX!r!aX!v!aXv!aX~OQyOutP~Oz|O!r!aX!v!aXv!aX~OPeORUOSVOmUOnUOoUOpUO!fSO!hTO~Oa!QO~O!r!RO!v!RO~OsXOw!TO~P&yOsXOwfOygOzVa!rVa!vVa!gVavVa~P&yOa!XOb!XOc!XOd!XOe!XOf!XOg!YOh!YO~O[kO]kO^lO_lO~P(mO[kO]kO^lO_lO!g!ZO~O!g!ZO[!dX]!dX^!dX_!dXa!dXb!dXc!dXd!dXe!dXf!dXg!dXh!dX~Oz|O!g!ZO~OP![O!fSO~O!h!]O!j!]O!k!]O!l!]O!m!]O!n!]O~OjtO!h!_O!jrO!ksO~OP!`O~OQyOutX~Ou!bO~OP!cO~Oz|O!rUa!vUa!gUavUa~Ou!fO~P(mOu!fO~OQ_OsXO|]O~P$xO[kO]kO^Zi_Zi!rZi!vZi!gZivZi~OQ_OsXO|]O!r!kO~P$xOQ_OsXO|]O!r!nO~P$xO!g`iu`i~P(mOv!oO~OQ_OsXO|]Ov!sP~P$xOQ_OsXO|]Ov!sP!Q!sP!S!sP~P$xO!r!uO~OQ_OsXO|]Ov!sX!Q!sX!S!sX~P$xOv!wO~Ov!|O!Q!xO!S!{O~Ov#RO!Q!xO!S!{O~Ou#TO~Ov#RO~Ou#UO~P(mOu#UO~Ov#VO~O!r#WO~O!r#XO~Om_o]o~", stateData: "1s~O!_OS~O]PO^_O_UO`VOmUOnUOoUOpUOsXO|]O!fSO!hTO!rbO~O]eO_UO`VOmUOnUOoUOpUOsXOwfOygO!fSO!hTOzfX!rfX!vfX!gfXvfX~OP!dXQ!dXR!dXS!dXT!dXU!dXV!dXW!dXX!dXY!dXZ!dX[!dX~P!QOPkOQkORlOSlO~OPkOQkORlOSlO!r!aX!v!aXv!aX~O]PO_UO`VOmUOnUOoUOpUO!fSO!hTO~OjtO!hwO!jrO!ksO~O!oxO~OP!dXQ!dXR!dXS!dX!r!aX!v!aXv!aX~O^yOutP~Oz|O!r!aX!v!aXv!aX~O]eO_UO`VOmUOnUOoUOpUO!fSO!hTO~OV!QO~O!r!RO!v!RO~OsXOw!TO~P&yOsXOwfOygOzca!rca!vca!gcavca~P&yOT!YOU!YOV!XOW!XOX!XOY!XOZ!XO[!XO~OPkOQkORlOSlO~P(mOPkOQkORlOSlO!g!ZO~O!g!ZOP!dXQ!dXR!dXS!dXT!dXU!dXV!dXW!dXX!dXY!dXZ!dX[!dX~Oz|O!g!ZO~O]![O!fSO~O!h!]O!j!]O!k!]O!l!]O!m!]O!n!]O~OjtO!h!_O!jrO!ksO~O]!`O~O^yOutX~Ou!bO~O]!cO~Oz|O!rba!vba!gbavba~Ou!fO~P(mOu!fO~O^_OsXO|]O~P$xOPkOQkORgiSgi!rgi!vgi!ggivgi~O^_OsXO|]O!r!kO~P$xO^_OsXO|]O!r!nO~P$xO!ghiuhi~P(mOv!oO~O^_OsXO|]Ov!sP~P$xO^_OsXO|]Ov!sP!Q!sP!S!sP~P$xO!r!uO~O^_OsXO|]Ov!sX!Q!sX!S!sX~P$xOv!wO~Ov!|O!Q!xO!S!{O~Ov#RO!Q!xO!S!{O~Ou#TO~Ov#RO~Ou#UO~P(mOu#UO~Ov#VO~O!r#WO~O!r#XO~Omo~",
goto: "+m!vPPPPPP!w#W#f#k#W$VPPPP$lPPPPPPPP$xP%a%aPPPP%e&OP&dPPP#fPP&gP&s&v'PP'TP&g'Z'a'h'n't'}(UPPP([(`(t)W)]*WPPP*sPPPPPP*w*wP+X+a+ad`Od!Q!b!f!k!n!q#W#XRpSiZOSd|!Q!b!f!k!n!q#W#XVhPj!czUOPS]dgjkl!Q!X!Y!b!c!f!k!n!q!x#W#XR![rdROd!Q!b!f!k!n!q#W#XQnSQ!VkR!WlQpSQ!P]Q!h!YR#P!x{UOPS]dgjkl!Q!X!Y!b!c!f!k!n!q!x#W#XTtTvdWOd!Q!b!f!k!n!q#W#XgePS]gjkl!X!Y!c!xd`Od!Q!b!f!k!n!q#W#XUfPj!cR!TgR{Xe`Od!Q!b!f!k!n!q#W#XR!m!fQ!t!nQ#Y#WR#Z#XT!y!t!zQ!}!tR#S!zQdOR!SdSjP!cR!UjQvTR!^vQzXR!azW!q!k!n#W#XR!v!qS}[qR!e}Q!z!tR#Q!zTcOdSaOdQ!g!QQ!j!bQ!l!fZ!p!k!n!q#W#Xd[Od!Q!b!f!k!n!q#W#XQqSR!d|ViPj!cdQOd!Q!b!f!k!n!q#W#XUfPj!cQmSQ!O]Q!TgQ!VkQ!WlQ!h!XQ!i!YR#O!xdWOd!Q!b!f!k!n!q#W#XdeP]gjkl!X!Y!c!xRoSTuTvmYOPdgj!Q!b!c!f!k!n!q#W#XQ!r!kV!s!n#W#Xe^Od!Q!b!f!k!n!q#W#X", goto: "+m!vPPPPPPPPPPPPPPPPPP!w#W#f#k#W$V$l$xP%a%aPPPP%e&OP&dPPP#fPP&gP&s&v'PP'TP&g'Z'a'h'n't'}(UPPP([(`(t)W)]*WPPP*sPPPPPP*w*wP+X+a+ad`Od!Q!b!f!k!n!q#W#XRpSiZOSd|!Q!b!f!k!n!q#W#XVhPj!czUOPS]dgjkl!Q!X!Y!b!c!f!k!n!q!x#W#XR![rdROd!Q!b!f!k!n!q#W#XQnSQ!VkR!WlQpSQ!P]Q!h!YR#P!x{UOPS]dgjkl!Q!X!Y!b!c!f!k!n!q!x#W#XTtTvdWOd!Q!b!f!k!n!q#W#XgePS]gjkl!X!Y!c!xd`Od!Q!b!f!k!n!q#W#XUfPj!cR!TgR{Xe`Od!Q!b!f!k!n!q#W#XR!m!fQ!t!nQ#Y#WR#Z#XT!y!t!zQ!}!tR#S!zQdOR!SdSjP!cR!UjQvTR!^vQzXR!azW!q!k!n#W#XR!v!qS}[qR!e}Q!z!tR#Q!zTcOdSaOdQ!g!QQ!j!bQ!l!fZ!p!k!n!q#W#Xd[Od!Q!b!f!k!n!q#W#XQqSR!d|ViPj!cdQOd!Q!b!f!k!n!q#W#XUfPj!cQmSQ!O]Q!TgQ!VkQ!WlQ!h!XQ!i!YR#O!xdWOd!Q!b!f!k!n!q#W#XdeP]gjkl!X!Y!c!xRoSTuTvmYOPdgj!Q!b!c!f!k!n!q#W#XQ!r!kV!s!n#W#Xe^Od!Q!b!f!k!n!q#W#X",
nodeNames: "⚠ Identifier AssignableIdentifier Word IdentifierBeforeDot Program PipeExpr FunctionCall PositionalArg ParenExpr FunctionCallOrIdentifier BinOp operator operator operator operator ConditionalOp operator operator operator operator operator operator operator operator String StringFragment Interpolation EscapeSeq Number Boolean Regex Null DotGet FunctionDef keyword Params colon end Underscore NamedArg NamedArgPrefix operator IfExpr keyword ThenBlock ThenBlock ElsifExpr keyword ElseExpr keyword Assign", nodeNames: "⚠ Star Slash Plus Minus And Or Eq Neq Lt Lte Gt Gte Identifier AssignableIdentifier Word IdentifierBeforeDot Program PipeExpr FunctionCall PositionalArg ParenExpr FunctionCallOrIdentifier BinOp ConditionalOp String StringFragment Interpolation EscapeSeq Number Boolean Regex Null DotGet FunctionDef keyword Params colon end Underscore NamedArg NamedArgPrefix operator IfExpr keyword ThenBlock ThenBlock ElsifExpr keyword ElseExpr keyword Assign",
maxTerm: 84, maxTerm: 84,
context: trackScope, context: trackScope,
nodeProps: [ nodeProps: [
@ -18,8 +19,8 @@ export const parser = LRParser.deserialize({
propSources: [highlighting], propSources: [highlighting],
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 7, repeatNodeCount: 7,
tokenData: "!&X~R!SOX$_XY$|YZ%gZp$_pq$|qr&Qrt$_tu'Yuw$_wx'_xy'dyz'}z{(h{|)R|}$_}!O)l!O!P,b!P!Q,{!Q![*]![!]5j!]!^%g!^!_6T!_!`7_!`!a7x!a#O$_#O#P9S#P#R$_#R#S9X#S#T$_#T#U9r#U#X;W#X#Y=m#Y#ZDs#Z#];W#]#^JO#^#b;W#b#cKp#c#d! Y#d#f;W#f#g!!z#g#h;W#h#i!#q#i#o;W#o#p$_#p#q!%i#q;'S$_;'S;=`$v<%l~$_~O$_~~!&SS$dUjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_S$yP;=`<%l$__%TUjS!_ZOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V%nUjS!rROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V&VWjSOt$_uw$_x!_$_!_!`&o!`#O$_#P;'S$_;'S;=`$v<%lO$_V&vUbRjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_~'_O!j~~'dO!h~V'kUjS!fROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V(UUjS!gROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V(oU[RjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V)YU^RjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V)sWjS_ROt$_uw$_x!Q$_!Q![*]![#O$_#P;'S$_;'S;=`$v<%lO$_V*dYjSmROt$_uw$_x!O$_!O!P+S!P!Q$_!Q![*]![#O$_#P;'S$_;'S;=`$v<%lO$_V+XWjSOt$_uw$_x!Q$_!Q![+q![#O$_#P;'S$_;'S;=`$v<%lO$_V+xWjSmROt$_uw$_x!Q$_!Q![+q![#O$_#P;'S$_;'S;=`$v<%lO$_T,iU!oPjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V-SWjS]ROt$_uw$_x!P$_!P!Q-l!Q#O$_#P;'S$_;'S;=`$v<%lO$_V-q^jSOY.mYZ$_Zt.mtu/puw.mwx/px!P.m!P!Q$_!Q!}.m!}#O4c#O#P2O#P;'S.m;'S;=`5d<%lO.mV.t^jSoROY.mYZ$_Zt.mtu/puw.mwx/px!P.m!P!Q2e!Q!}.m!}#O4c#O#P2O#P;'S.m;'S;=`5d<%lO.mR/uXoROY/pZ!P/p!P!Q0b!Q!}/p!}#O1P#O#P2O#P;'S/p;'S;=`2_<%lO/pR0eP!P!Q0hR0mUoR#Z#[0h#]#^0h#a#b0h#g#h0h#i#j0h#m#n0hR1SVOY1PZ#O1P#O#P1i#P#Q/p#Q;'S1P;'S;=`1x<%lO1PR1lSOY1PZ;'S1P;'S;=`1x<%lO1PR1{P;=`<%l1PR2RSOY/pZ;'S/p;'S;=`2_<%lO/pR2bP;=`<%l/pV2jWjSOt$_uw$_x!P$_!P!Q3S!Q#O$_#P;'S$_;'S;=`$v<%lO$_V3ZbjSoROt$_uw$_x#O$_#P#Z$_#Z#[3S#[#]$_#]#^3S#^#a$_#a#b3S#b#g$_#g#h3S#h#i$_#i#j3S#j#m$_#m#n3S#n;'S$_;'S;=`$v<%lO$_V4h[jSOY4cYZ$_Zt4ctu1Puw4cwx1Px#O4c#O#P1i#P#Q.m#Q;'S4c;'S;=`5^<%lO4cV5aP;=`<%l4cV5gP;=`<%l.mT5qUjSuPOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V6[WcRjSOt$_uw$_x!_$_!_!`6t!`#O$_#P;'S$_;'S;=`$v<%lO$_V6{UdRjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V7fUaRjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V8PWeRjSOt$_uw$_x!_$_!_!`8i!`#O$_#P;'S$_;'S;=`$v<%lO$_V8pUfRjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_~9XO!k~V9`UjSwROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V9w[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#b;W#b#c;{#c#o;W#o;'S$_;'S;=`$v<%lO$_U:tUyQjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_U;]YjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_V<Q[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#W;W#W#X<v#X#o;W#o;'S$_;'S;=`$v<%lO$_V<}YgRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_V=r^jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#`;W#`#a>n#a#b;W#b#cCR#c#o;W#o;'S$_;'S;=`$v<%lO$_V>s[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#g;W#g#h?i#h#o;W#o;'S$_;'S;=`$v<%lO$_V?n^jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#X;W#X#Y@j#Y#];W#]#^Aa#^#o;W#o;'S$_;'S;=`$v<%lO$_V@qY!SPjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VAf[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#Y;W#Y#ZB[#Z#o;W#o;'S$_;'S;=`$v<%lO$_VBcY!QPjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VCW[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#W;W#W#XC|#X#o;W#o;'S$_;'S;=`$v<%lO$_VDTYjSvROt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VDx]jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#UEq#U#b;W#b#cIX#c#o;W#o;'S$_;'S;=`$v<%lO$_VEv[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#`;W#`#aFl#a#o;W#o;'S$_;'S;=`$v<%lO$_VFq[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#g;W#g#hGg#h#o;W#o;'S$_;'S;=`$v<%lO$_VGl[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#X;W#X#YHb#Y#o;W#o;'S$_;'S;=`$v<%lO$_VHiYnRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VI`YsRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VJT[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#Y;W#Y#ZJy#Z#o;W#o;'S$_;'S;=`$v<%lO$_VKQY|PjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$__Kw[!lWjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#i;W#i#jLm#j#o;W#o;'S$_;'S;=`$v<%lO$_VLr[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#`;W#`#aMh#a#o;W#o;'S$_;'S;=`$v<%lO$_VMm[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#`;W#`#aNc#a#o;W#o;'S$_;'S;=`$v<%lO$_VNjYpRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_V! _[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#f;W#f#g!!T#g#o;W#o;'S$_;'S;=`$v<%lO$_V!![YhRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_^!#RY!nWjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$__!#x[!mWjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#f;W#f#g!$n#g#o;W#o;'S$_;'S;=`$v<%lO$_V!$s[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#i;W#i#jGg#j#o;W#o;'S$_;'S;=`$v<%lO$_V!%pUzRjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_~!&XO!v~", tokenData: "JX~R|OX#{XY$jYZ%TZp#{pq$jqt#{tu%nuw#{wx%sxy%xyz&cz{#{{|&||}#{}!O&|!O!P)p!P!Q*Z!Q!['k![!]2v!]!^%T!^#O#{#O#P3a#P#R#{#R#S3f#S#T#{#T#X4P#X#Y5_#Y#Z<e#Z#]4P#]#^Ap#^#b4P#b#cCb#c#f4P#f#gFz#g#h4P#h#iGq#i#o4P#o#p#{#p#qIi#q;'S#{;'S;=`$d<%l~#{~O#{~~JSS$QUjSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{_$qUjS!_ZOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{V%[UjS!rROt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~%sO!j~~%xO!h~V&PUjS!fROt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{V&jUjS!gROt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{V'RWjSOt#{uw#{x!Q#{!Q!['k![#O#{#P;'S#{;'S;=`$d<%lO#{V'rYjSmROt#{uw#{x!O#{!O!P(b!P!Q#{!Q!['k![#O#{#P;'S#{;'S;=`$d<%lO#{V(gWjSOt#{uw#{x!Q#{!Q![)P![#O#{#P;'S#{;'S;=`$d<%lO#{V)WWjSmROt#{uw#{x!Q#{!Q![)P![#O#{#P;'S#{;'S;=`$d<%lO#{T)wU!oPjSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{V*`WjSOt#{uw#{x!P#{!P!Q*x!Q#O#{#P;'S#{;'S;=`$d<%lO#{V*}^jSOY+yYZ#{Zt+ytu,|uw+ywx,|x!P+y!P!Q#{!Q!}+y!}#O1o#O#P/[#P;'S+y;'S;=`2p<%lO+yV,Q^jSoROY+yYZ#{Zt+ytu,|uw+ywx,|x!P+y!P!Q/q!Q!}+y!}#O1o#O#P/[#P;'S+y;'S;=`2p<%lO+yR-RXoROY,|Z!P,|!P!Q-n!Q!},|!}#O.]#O#P/[#P;'S,|;'S;=`/k<%lO,|R-qP!P!Q-tR-yUoR#Z#[-t#]#^-t#a#b-t#g#h-t#i#j-t#m#n-tR.`VOY.]Z#O.]#O#P.u#P#Q,|#Q;'S.];'S;=`/U<%lO.]R.xSOY.]Z;'S.];'S;=`/U<%lO.]R/XP;=`<%l.]R/_SOY,|Z;'S,|;'S;=`/k<%lO,|R/nP;=`<%l,|V/vWjSOt#{uw#{x!P#{!P!Q0`!Q#O#{#P;'S#{;'S;=`$d<%lO#{V0gbjSoROt#{uw#{x#O#{#P#Z#{#Z#[0`#[#]#{#]#^0`#^#a#{#a#b0`#b#g#{#g#h0`#h#i#{#i#j0`#j#m#{#m#n0`#n;'S#{;'S;=`$d<%lO#{V1t[jSOY1oYZ#{Zt1otu.]uw1owx.]x#O1o#O#P.u#P#Q+y#Q;'S1o;'S;=`2j<%lO1oV2mP;=`<%l1oV2sP;=`<%l+yT2}UjSuPOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~3fO!k~V3mUjSwROt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4UYjSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#o4P#o;'S#{;'S;=`$d<%lO#{U4{UyQjSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{V5d^jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#`4P#`#a6`#a#b4P#b#c:s#c#o4P#o;'S#{;'S;=`$d<%lO#{V6e[jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#g4P#g#h7Z#h#o4P#o;'S#{;'S;=`$d<%lO#{V7`^jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#X4P#X#Y8[#Y#]4P#]#^9R#^#o4P#o;'S#{;'S;=`$d<%lO#{V8cY!SPjSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#o4P#o;'S#{;'S;=`$d<%lO#{V9W[jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#Y4P#Y#Z9|#Z#o4P#o;'S#{;'S;=`$d<%lO#{V:TY!QPjSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#o4P#o;'S#{;'S;=`$d<%lO#{V:x[jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#W4P#W#X;n#X#o4P#o;'S#{;'S;=`$d<%lO#{V;uYjSvROt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#o4P#o;'S#{;'S;=`$d<%lO#{V<j]jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#U=c#U#b4P#b#c@y#c#o4P#o;'S#{;'S;=`$d<%lO#{V=h[jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#`4P#`#a>^#a#o4P#o;'S#{;'S;=`$d<%lO#{V>c[jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#g4P#g#h?X#h#o4P#o;'S#{;'S;=`$d<%lO#{V?^[jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#X4P#X#Y@S#Y#o4P#o;'S#{;'S;=`$d<%lO#{V@ZYnRjSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#o4P#o;'S#{;'S;=`$d<%lO#{VAQYsRjSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#o4P#o;'S#{;'S;=`$d<%lO#{VAu[jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#Y4P#Y#ZBk#Z#o4P#o;'S#{;'S;=`$d<%lO#{VBrY|PjSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#o4P#o;'S#{;'S;=`$d<%lO#{_Ci[!lWjSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#i4P#i#jD_#j#o4P#o;'S#{;'S;=`$d<%lO#{VDd[jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#`4P#`#aEY#a#o4P#o;'S#{;'S;=`$d<%lO#{VE_[jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#`4P#`#aFT#a#o4P#o;'S#{;'S;=`$d<%lO#{VF[YpRjSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#o4P#o;'S#{;'S;=`$d<%lO#{^GRY!nWjSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#o4P#o;'S#{;'S;=`$d<%lO#{_Gx[!mWjSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#f4P#f#gHn#g#o4P#o;'S#{;'S;=`$d<%lO#{VHs[jSOt#{uw#{x!_#{!_!`4t!`#O#{#P#T#{#T#i4P#i#j?X#j#o4P#o;'S#{;'S;=`$d<%lO#{VIpUzRjSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~JXO!v~",
tokenizers: [tokenizer, 0, 1, 2, 3], tokenizers: [operatorTokenizer, 0, 1, 2, 3, tokenizer],
topRules: {"Program":[0,5]}, topRules: {"Program":[0,17]},
tokenPrec: 768 tokenPrec: 768
}) })

View File

@ -11,7 +11,7 @@ describe('null', () => {
expect('a = null').toMatchTree(` expect('a = null').toMatchTree(`
Assign Assign
AssignableIdentifier a AssignableIdentifier a
operator = Eq =
Null null`) Null null`)
}) })
}) })
@ -30,7 +30,7 @@ describe('Parentheses', () => {
ParenExpr ParenExpr
BinOp BinOp
Number 2 Number 2
operator + Plus +
Number 3`) Number 3`)
}) })
@ -72,14 +72,14 @@ describe('Parentheses', () => {
ParenExpr ParenExpr
ConditionalOp ConditionalOp
Identifier a Identifier a
operator > Gt >
Identifier b`) Identifier b`)
expect('(a and b)').toMatchTree(` expect('(a and b)').toMatchTree(`
ParenExpr ParenExpr
ConditionalOp ConditionalOp
Identifier a Identifier a
operator and And and
Identifier b`) Identifier b`)
}) })
@ -91,7 +91,7 @@ describe('Parentheses', () => {
ParenExpr ParenExpr
BinOp BinOp
Number 3 Number 3
operator + Plus +
Number 3`) Number 3`)
}) })
@ -105,13 +105,16 @@ describe('Parentheses', () => {
`) `)
}) })
test('a word start with *', () => { test('a word start with an operator', () => {
expect('find *cool*').toMatchTree(` const operators = ['*', '/', '+', '-', 'and', 'or', '=', '!=', '>=', '<=', '>', '<']
FunctionCall for (const operator of operators) {
Identifier find expect(`find ${operator}cool*`).toMatchTree(`
PositionalArg FunctionCall
Word *cool* Identifier find
PositionalArg
Word ${operator}cool*
`) `)
}
}) })
test('a word can look like a binop', () => { test('a word can look like a binop', () => {
@ -128,11 +131,11 @@ describe('Parentheses', () => {
ParenExpr ParenExpr
BinOp BinOp
Number 2 Number 2
operator + Plus +
ParenExpr ParenExpr
BinOp BinOp
Number 1 Number 1
operator * Star *
Number 4`) Number 4`)
}) })
@ -140,7 +143,7 @@ describe('Parentheses', () => {
expect('4 + (echo 3)').toMatchTree(` expect('4 + (echo 3)').toMatchTree(`
BinOp BinOp
Number 4 Number 4
operator + Plus +
ParenExpr ParenExpr
FunctionCall FunctionCall
Identifier echo Identifier echo
@ -149,12 +152,12 @@ describe('Parentheses', () => {
}) })
}) })
describe.only('BinOp', () => { describe('BinOp', () => {
test('addition tests', () => { test('addition tests', () => {
expect('2 + 3').toMatchTree(` expect('2 + 3').toMatchTree(`
BinOp BinOp
Number 2 Number 2
operator + Plus +
Number 3 Number 3
`) `)
}) })
@ -163,7 +166,7 @@ describe.only('BinOp', () => {
expect('5 - 2').toMatchTree(` expect('5 - 2').toMatchTree(`
BinOp BinOp
Number 5 Number 5
operator - Minus -
Number 2 Number 2
`) `)
}) })
@ -172,7 +175,7 @@ describe.only('BinOp', () => {
expect('4 * 3').toMatchTree(` expect('4 * 3').toMatchTree(`
BinOp BinOp
Number 4 Number 4
operator * Star *
Number 3 Number 3
`) `)
}) })
@ -181,7 +184,7 @@ describe.only('BinOp', () => {
expect('8 / 2').toMatchTree(` expect('8 / 2').toMatchTree(`
BinOp BinOp
Number 8 Number 8
operator / Slash /
Number 2 Number 2
`) `)
}) })
@ -191,15 +194,15 @@ describe.only('BinOp', () => {
BinOp BinOp
BinOp BinOp
Number 2 Number 2
operator + Plus +
BinOp BinOp
Number 3 Number 3
operator * Star *
Number 4 Number 4
operator - Minus -
BinOp BinOp
Number 5 Number 5
operator / Slash /
Number 1 Number 1
`) `)
}) })
@ -210,7 +213,7 @@ describe('ambiguity', () => {
expect('a + -3').toMatchTree(` expect('a + -3').toMatchTree(`
BinOp BinOp
Identifier a Identifier a
operator + Plus +
Number -3 Number -3
`) `)
}) })
@ -219,7 +222,7 @@ describe('ambiguity', () => {
expect('a-var + a-thing').toMatchTree(` expect('a-var + a-thing').toMatchTree(`
BinOp BinOp
Identifier a-var Identifier a-var
operator + Plus +
Identifier a-thing Identifier a-thing
`) `)
}) })
@ -231,11 +234,11 @@ describe('newlines', () => {
y = 2`).toMatchTree(` y = 2`).toMatchTree(`
Assign Assign
AssignableIdentifier x AssignableIdentifier x
operator = Eq =
Number 5 Number 5
Assign Assign
AssignableIdentifier y AssignableIdentifier y
operator = Eq =
Number 2`) Number 2`)
}) })
@ -243,11 +246,11 @@ y = 2`).toMatchTree(`
expect(`x = 5; y = 2`).toMatchTree(` expect(`x = 5; y = 2`).toMatchTree(`
Assign Assign
AssignableIdentifier x AssignableIdentifier x
operator = Eq =
Number 5 Number 5
Assign Assign
AssignableIdentifier y AssignableIdentifier y
operator = Eq =
Number 2`) Number 2`)
}) })
@ -255,7 +258,7 @@ y = 2`).toMatchTree(`
expect(`a = hello; 2`).toMatchTree(` expect(`a = hello; 2`).toMatchTree(`
Assign Assign
AssignableIdentifier a AssignableIdentifier a
operator = Eq =
FunctionCallOrIdentifier FunctionCallOrIdentifier
Identifier hello Identifier hello
Number 2`) Number 2`)
@ -267,7 +270,7 @@ describe('Assign', () => {
expect('x = 5').toMatchTree(` expect('x = 5').toMatchTree(`
Assign Assign
AssignableIdentifier x AssignableIdentifier x
operator = Eq =
Number 5`) Number 5`)
}) })
@ -275,10 +278,10 @@ describe('Assign', () => {
expect('x = 5 + 3').toMatchTree(` expect('x = 5 + 3').toMatchTree(`
Assign Assign
AssignableIdentifier x AssignableIdentifier x
operator = Eq =
BinOp BinOp
Number 5 Number 5
operator + Plus +
Number 3`) Number 3`)
}) })
@ -286,7 +289,7 @@ describe('Assign', () => {
expect('add = fn a b: a + b end').toMatchTree(` expect('add = fn a b: a + b end').toMatchTree(`
Assign Assign
AssignableIdentifier add AssignableIdentifier add
operator = Eq =
FunctionDef FunctionDef
keyword fn keyword fn
Params Params
@ -295,7 +298,7 @@ describe('Assign', () => {
colon : colon :
BinOp BinOp
Identifier a Identifier a
operator + Plus +
Identifier b Identifier b
end end`) end end`)
}) })
@ -306,7 +309,7 @@ describe('DotGet whitespace sensitivity', () => {
expect('basename = 5; basename.prop').toMatchTree(` expect('basename = 5; basename.prop').toMatchTree(`
Assign Assign
AssignableIdentifier basename AssignableIdentifier basename
operator = Eq =
Number 5 Number 5
DotGet DotGet
IdentifierBeforeDot basename IdentifierBeforeDot basename
@ -317,11 +320,11 @@ describe('DotGet whitespace sensitivity', () => {
expect('basename = 5; basename / prop').toMatchTree(` expect('basename = 5; basename / prop').toMatchTree(`
Assign Assign
AssignableIdentifier basename AssignableIdentifier basename
operator = Eq =
Number 5 Number 5
BinOp BinOp
Identifier basename Identifier basename
operator / Slash /
Identifier prop`) Identifier prop`)
}) })

View File

@ -9,7 +9,7 @@ describe('if/elsif/else', () => {
keyword if keyword if
ConditionalOp ConditionalOp
Identifier y Identifier y
operator = Eq =
Number 1 Number 1
colon : colon :
ThenBlock ThenBlock
@ -20,7 +20,7 @@ describe('if/elsif/else', () => {
expect('a = if x: 2').toMatchTree(` expect('a = if x: 2').toMatchTree(`
Assign Assign
AssignableIdentifier a AssignableIdentifier a
operator = Eq =
IfExpr IfExpr
keyword if keyword if
Identifier x Identifier x
@ -39,7 +39,7 @@ describe('if/elsif/else', () => {
keyword if keyword if
ConditionalOp ConditionalOp
Identifier x Identifier x
operator < Lt <
Number 9 Number 9
colon : colon :
ThenBlock ThenBlock

View File

@ -18,7 +18,7 @@ describe('DotGet', () => {
expect('obj = 5; obj.prop').toMatchTree(` expect('obj = 5; obj.prop').toMatchTree(`
Assign Assign
AssignableIdentifier obj AssignableIdentifier obj
operator = Eq =
Number 5 Number 5
DotGet DotGet
IdentifierBeforeDot obj IdentifierBeforeDot obj
@ -106,7 +106,7 @@ end`).toMatchTree(`
expect('config = 42; echo config.path').toMatchTree(` expect('config = 42; echo config.path').toMatchTree(`
Assign Assign
AssignableIdentifier config AssignableIdentifier config
operator = Eq =
Number 42 Number 42
FunctionCall FunctionCall
Identifier echo Identifier echo
@ -121,7 +121,7 @@ end`).toMatchTree(`
expect('config = 42; cat readme.txt; echo config.path').toMatchTree(` expect('config = 42; cat readme.txt; echo config.path').toMatchTree(`
Assign Assign
AssignableIdentifier config AssignableIdentifier config
operator = Eq =
Number 42 Number 42
FunctionCall FunctionCall
Identifier cat Identifier cat

View File

@ -76,7 +76,7 @@ describe('Fn', () => {
colon : colon :
BinOp BinOp
Identifier x Identifier x
operator + Plus +
Number 1 Number 1
end end`) end end`)
}) })
@ -91,7 +91,7 @@ describe('Fn', () => {
colon : colon :
BinOp BinOp
Identifier x Identifier x
operator * Star *
Identifier y Identifier y
end end`) end end`)
}) })
@ -109,11 +109,11 @@ end`).toMatchTree(`
colon : colon :
BinOp BinOp
Identifier x Identifier x
operator * Star *
Identifier y Identifier y
BinOp BinOp
Identifier x Identifier x
operator + Plus +
Number 9 Number 9
end end`) end end`)
}) })

View File

@ -22,7 +22,7 @@ describe('multiline', () => {
`).toMatchTree(` `).toMatchTree(`
Assign Assign
AssignableIdentifier add AssignableIdentifier add
operator = Eq =
FunctionDef FunctionDef
keyword fn keyword fn
Params Params
@ -31,10 +31,10 @@ describe('multiline', () => {
colon : colon :
Assign Assign
AssignableIdentifier result AssignableIdentifier result
operator = Eq =
BinOp BinOp
Identifier a Identifier a
operator + Plus +
Identifier b Identifier b
FunctionCallOrIdentifier FunctionCallOrIdentifier
Identifier result Identifier result

View File

@ -51,7 +51,7 @@ describe('pipe expressions', () => {
expect('result = echo hello | grep h').toMatchTree(` expect('result = echo hello | grep h').toMatchTree(`
Assign Assign
AssignableIdentifier result AssignableIdentifier result
operator = Eq =
PipeExpr PipeExpr
FunctionCall FunctionCall
Identifier echo Identifier echo

View File

@ -20,7 +20,7 @@ describe('string interpolation', () => {
ParenExpr ParenExpr
BinOp BinOp
Identifier a Identifier a
operator + Plus +
Identifier b Identifier b
StringFragment ! StringFragment !
`) `)
@ -34,7 +34,7 @@ describe('string interpolation', () => {
ParenExpr ParenExpr
BinOp BinOp
Identifier a Identifier a
operator + Plus +
Identifier b Identifier b
`) `)
}) })

View File

@ -8,6 +8,12 @@ export const tokenizer = new ExternalTokenizer(
const ch = getFullCodePoint(input, 0) const ch = getFullCodePoint(input, 0)
if (!isWordChar(ch)) return if (!isWordChar(ch)) return
// Don't consume things that start with digits - let Number token handle it
if (isDigit(ch)) return
// Don't consume things that start with - or + followed by a digit (negative/positive numbers)
if ((ch === 45 /* - */ || ch === 43) /* + */ && isDigit(input.peek(1))) return
const isValidStart = isLowercaseLetter(ch) || isEmoji(ch) const isValidStart = isLowercaseLetter(ch) || isEmoji(ch)
const canBeWord = stack.canShift(Word) const canBeWord = stack.canShift(Word)
@ -166,7 +172,16 @@ const chooseIdentifierToken = (input: InputStream, stack: Stack): number => {
} }
const nextCh = getFullCodePoint(input, peekPos) const nextCh = getFullCodePoint(input, peekPos)
return nextCh === 61 /* = */ ? AssignableIdentifier : Identifier if (nextCh === 61 /* = */) {
// Found '=', but check if it's followed by whitespace
// If '=' is followed by non-whitespace (like '=cool*'), it won't be tokenized as Eq
// In that case, this should be Identifier (for function call), not AssignableIdentifier
const charAfterEquals = getFullCodePoint(input, peekPos + 1)
if (isWhiteSpace(charAfterEquals) || charAfterEquals === -1 /* EOF */) {
return AssignableIdentifier
}
}
return Identifier
} }
// Character classification helpers // Character classification helpers

View File

@ -1,10 +1,10 @@
import { expect } from 'bun:test' import { expect } from 'bun:test'
import { Tree, TreeCursor } from '@lezer/common'
import { parser } from '#parser/shrimp' import { parser } from '#parser/shrimp'
import { $ } from 'bun' import { $ } from 'bun'
import { assert, assertNever, errorMessage } from '#utils/utils' import { assert, errorMessage } from '#utils/utils'
import { Compiler } from '#compiler/compiler' import { Compiler } from '#compiler/compiler'
import { run, VM, type Value } from 'reefvm' import { run, VM } from 'reefvm'
import { treeToString, VMResultToValue } from '#utils/tree'
const regenerateParser = async () => { const regenerateParser = async () => {
let generate = true let generate = true
@ -131,10 +131,11 @@ expect.extend({
try { try {
const compiler = new Compiler(received) const compiler = new Compiler(received)
const vm = new VM(compiler.bytecode) const vm = new VM(compiler.bytecode)
await vm.run() const value = await vm.run()
return { return {
message: () => `Expected evaluation to fail, but it succeeded.`, message: () =>
`Expected evaluation to fail, but it succeeded with ${JSON.stringify(value)}`,
pass: false, pass: false,
} }
} catch (error) { } catch (error) {
@ -146,38 +147,6 @@ expect.extend({
}, },
}) })
const treeToString = (tree: Tree, input: string): string => {
const lines: string[] = []
const addNode = (cursor: TreeCursor, depth: number) => {
if (!cursor.name) return
const indent = ' '.repeat(depth)
const text = input.slice(cursor.from, cursor.to)
const nodeName = cursor.name // Save the node name before moving cursor
if (cursor.firstChild()) {
lines.push(`${indent}${nodeName}`)
do {
addNode(cursor, depth + 1)
} while (cursor.nextSibling())
cursor.parent()
} else {
const cleanText = nodeName === 'String' ? text.slice(1, -1) : text
lines.push(`${indent}${nodeName} ${cleanText}`)
}
}
const cursor = tree.cursor()
if (cursor.firstChild()) {
do {
addNode(cursor, 0)
} while (cursor.nextSibling())
}
return lines.join('\n')
}
const trimWhitespace = (str: string): string => { const trimWhitespace = (str: string): string => {
const lines = str.split('\n').filter((line) => line.trim().length > 0) const lines = str.split('\n').filter((line) => line.trim().length > 0)
const firstLine = lines[0] const firstLine = lines[0]
@ -196,29 +165,3 @@ const trimWhitespace = (str: string): string => {
}) })
.join('\n') .join('\n')
} }
const VMResultToValue = (result: Value): unknown => {
if (
result.type === 'number' ||
result.type === 'boolean' ||
result.type === 'string' ||
result.type === 'regex'
) {
return result.value
} else if (result.type === 'null') {
return null
} else if (result.type === 'array') {
return result.value.map(VMResultToValue)
} else if (result.type === 'dict') {
const obj: Record<string, unknown> = {}
for (const [key, val] of Object.entries(result.value)) {
obj[key] = VMResultToValue(val)
}
return obj
} else if (result.type === 'function') {
return Function
} else {
assertNever(result)
}
}

61
src/utils/tree.ts Normal file
View File

@ -0,0 +1,61 @@
import { Tree, TreeCursor } from '@lezer/common'
import { assertNever } from '#utils/utils'
import { type Value } from 'reefvm'
export const treeToString = (tree: Tree, input: string): string => {
const lines: string[] = []
const addNode = (cursor: TreeCursor, depth: number) => {
if (!cursor.name) return
const indent = ' '.repeat(depth)
const text = input.slice(cursor.from, cursor.to)
const nodeName = cursor.name // Save the node name before moving cursor
if (cursor.firstChild()) {
lines.push(`${indent}${nodeName}`)
do {
addNode(cursor, depth + 1)
} while (cursor.nextSibling())
cursor.parent()
} else {
const cleanText = nodeName === 'String' ? text.slice(1, -1) : text
lines.push(`${indent}${nodeName} ${cleanText}`)
}
}
const cursor = tree.cursor()
if (cursor.firstChild()) {
do {
addNode(cursor, 0)
} while (cursor.nextSibling())
}
return lines.join('\n')
}
export const VMResultToValue = (result: Value): unknown => {
if (
result.type === 'number' ||
result.type === 'boolean' ||
result.type === 'string' ||
result.type === 'regex'
) {
return result.value
} else if (result.type === 'null') {
return null
} else if (result.type === 'array') {
return result.value.map(VMResultToValue)
} else if (result.type === 'dict') {
const obj: Record<string, unknown> = {}
for (const [key, val] of Object.entries(result.value)) {
obj[key] = VMResultToValue(val)
}
return obj
} else if (result.type === 'function') {
return Function
} else {
assertNever(result)
}
}