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', () => {
expect('//[unclosed//').toFailEvaluation()
expect('//[unclosed//').toEvaluateTo('//[unclosed//')
})
})

View File

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

View File

@ -1,3 +1,4 @@
import { multilineModeSignal, outputSignal } from '#editor/editor'
import { printBytecodeOutput, printParserOutput, runCode } from '#editor/runCode'
import { EditorState } from '@codemirror/state'
import { keymap } from '@codemirror/view'
@ -27,9 +28,13 @@ const customKeymap = keymap.of([
if (multilineMode) {
const input = view.state.doc.toString()
runCode(input)
return true
} else {
outputSignal.emit('Press Shift+Enter to insert run the code.')
}
multilineModeSignal.emit(true)
multilineMode = true
view.dispatch({
changes: { from: view.state.doc.length, insert: '\n' },
@ -44,6 +49,10 @@ const customKeymap = keymap.of([
key: 'Tab',
preventDefault: true,
run: (view) => {
view.dispatch({
changes: { from: view.state.selection.main.from, insert: ' ' },
selection: { anchor: view.state.selection.main.from + 2 },
})
return true
},
},
@ -51,6 +60,8 @@ const customKeymap = keymap.of([
{
key: 'ArrowUp',
run: (view) => {
if (multilineMode) return false
const command = history.previous()
if (command === undefined) return false
view.dispatch({
@ -64,6 +75,8 @@ const customKeymap = keymap.of([
{
key: 'ArrowDown',
run: (view) => {
if (multilineMode) return false
const command = history.next()
if (command === undefined) return false
view.dispatch({
@ -124,9 +137,22 @@ export const shrimpKeymap = [customKeymap, singleLineFilter]
class History {
private commands: string[] = []
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) {
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
}

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)',
padding: '0px',
},
'.cm-activeLine': {
backgroundColor: 'transparent',
},
'&.cm-focused .cm-cursor': {
borderLeftColor: 'var(--caret)',
},
'&.cm-focused .cm-selectionBackground, ::selection': {
backgroundColor: 'var(--bg-selection)',
},
'.cm-gutters': {
display: 'none',
},
'.cm-editor': {
border: 'none',
outline: 'none',

View File

@ -1,9 +1,10 @@
import { outputSignal, errorSignal } from '#editor/editor'
import { Compiler } from '#compiler/compiler'
import { errorMessage, log } from '#utils/utils'
import { bytecodeToString, run } from 'reefvm'
import { bytecodeToString } from 'reefvm'
import { parser } from '#parser/shrimp'
import { sendToNose } from '#editor/noseClient'
import { treeToString } from '#utils/tree'
export const runCode = async (input: string) => {
try {
@ -18,7 +19,8 @@ export const runCode = async (input: string) => {
export const printParserOutput = (input: string) => {
try {
const cst = parser.parse(input)
outputSignal.emit(cst.toString())
const string = treeToString(cst, input)
outputSignal.emit(string)
} catch (error) {
log.error(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* }
@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 {
@precedence { Number "-" Regex "/"}
@precedence { Number Regex }
StringFragment { !['\\$]+ }
NamedArgPrefix { $[a-z]+ "=" }
Number { "-"? $[0-9]+ ('.' $[0-9]+)? }
Number { ("-" | "+")? $[0-9]+ ('.' $[0-9]+)? }
Boolean { "true" | "false" }
newlineOrSemicolon { "\n" | ";" }
eof { @eof }
@ -28,23 +28,13 @@
Fn[@name=keyword] { "fn" }
"if" [@name=keyword]
"elsif" [@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]
"else" [@name=keyword]
"|"[@name=operator]
}
@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot }
@precedence {
pipe @left,
multiplicative @left,
@ -140,14 +130,14 @@ ThenBlock {
}
ConditionalOp {
expression "=" expression |
expression "!=" expression |
expression "<" expression |
expression "<=" expression |
expression ">" expression |
expression ">=" expression |
expression "and" (expression | ConditionalOp) |
expression "or" (expression | ConditionalOp)
expression Eq expression |
expression Neq expression |
expression Lt expression |
expression Lte expression |
expression Gt expression |
expression Gte expression |
expression And (expression | ConditionalOp) |
expression Or (expression | ConditionalOp)
}
Params {
@ -155,14 +145,14 @@ Params {
}
Assign {
AssignableIdentifier "=" consumeToTerminator
AssignableIdentifier Eq consumeToTerminator
}
BinOp {
(expression | BinOp) !multiplicative "*" (expression | BinOp) |
(expression | BinOp) !multiplicative "/" (expression | BinOp) |
(expression | BinOp) !additive "+" (expression | BinOp) |
(expression | BinOp) !additive "-" (expression | BinOp)
(expression | BinOp) !multiplicative Star (expression | BinOp) |
(expression | BinOp) !multiplicative Slash (expression | BinOp) |
(expression | BinOp) !additive Plus (expression | BinOp) |
(expression | BinOp) !additive Minus (expression | BinOp)
}
ParenExpr {

View File

@ -1,17 +1,29 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Identifier = 1,
AssignableIdentifier = 2,
Word = 3,
IdentifierBeforeDot = 4,
Program = 5,
PipeExpr = 6,
FunctionCall = 7,
PositionalArg = 8,
ParenExpr = 9,
FunctionCallOrIdentifier = 10,
BinOp = 11,
ConditionalOp = 16,
Star = 1,
Slash = 2,
Plus = 3,
Minus = 4,
And = 5,
Or = 6,
Eq = 7,
Neq = 8,
Lt = 9,
Lte = 10,
Gt = 11,
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,
StringFragment = 26,
Interpolation = 27,

View File

@ -1,14 +1,15 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {operatorTokenizer} from "./operatorTokenizer"
import {tokenizer} from "./tokenizer"
import {trackScope} from "./scopeTracker"
import {highlighting} from "./highlight"
export const parser = LRParser.deserialize({
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",
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~",
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",
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",
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: "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!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: "⚠ 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,
context: trackScope,
nodeProps: [
@ -18,8 +19,8 @@ export const parser = LRParser.deserialize({
propSources: [highlighting],
skippedNodes: [0],
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~",
tokenizers: [tokenizer, 0, 1, 2, 3],
topRules: {"Program":[0,5]},
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: [operatorTokenizer, 0, 1, 2, 3, tokenizer],
topRules: {"Program":[0,17]},
tokenPrec: 768
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,12 @@ export const tokenizer = new ExternalTokenizer(
const ch = getFullCodePoint(input, 0)
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 canBeWord = stack.canShift(Word)
@ -166,7 +172,16 @@ const chooseIdentifierToken = (input: InputStream, stack: Stack): number => {
}
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

View File

@ -1,10 +1,10 @@
import { expect } from 'bun:test'
import { Tree, TreeCursor } from '@lezer/common'
import { parser } from '#parser/shrimp'
import { $ } from 'bun'
import { assert, assertNever, errorMessage } from '#utils/utils'
import { assert, errorMessage } from '#utils/utils'
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 () => {
let generate = true
@ -131,10 +131,11 @@ expect.extend({
try {
const compiler = new Compiler(received)
const vm = new VM(compiler.bytecode)
await vm.run()
const value = await vm.run()
return {
message: () => `Expected evaluation to fail, but it succeeded.`,
message: () =>
`Expected evaluation to fail, but it succeeded with ${JSON.stringify(value)}`,
pass: false,
}
} 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 lines = str.split('\n').filter((line) => line.trim().length > 0)
const firstLine = lines[0]
@ -196,29 +165,3 @@ const trimWhitespace = (str: string): string => {
})
.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)
}
}