Compare commits

...

2 Commits

Author SHA1 Message Date
e915868b7c interpolation in { curly strings } 2025-11-06 21:04:23 -08:00
5b965326e4 curly -> Curly 2025-11-06 13:32:31 -08:00
9 changed files with 185 additions and 71 deletions

View File

@ -2,6 +2,7 @@ import { CompilerError } from '#compiler/compilerError.ts'
import { parser } from '#parser/shrimp.ts'
import * as terms from '#parser/shrimp.terms'
import { setGlobals } from '#parser/tokenizer'
import { tokenizeCurlyString } from '#parser/curlyTokenizer'
import type { SyntaxNode, Tree } from '@lezer/common'
import { assert, errorMessage } from '#utils/utils'
import { toBytecode, type Bytecode, type ProgramItem, bytecodeToString } from 'reefvm'
@ -112,6 +113,9 @@ export class Compiler {
return [[`PUSH`, number]]
case terms.String: {
if (node.firstChild?.type.id === terms.CurlyString)
return this.#compileCurlyString(value, input)
const { parts, hasInterpolation } = getStringParts(node, input)
// Simple string without interpolation or escapes - extract text directly
@ -772,4 +776,26 @@ export class Compiler {
return instructions
}
#compileCurlyString(value: string, input: string): ProgramItem[] {
const instructions: ProgramItem[] = []
const nodes = tokenizeCurlyString(value)
nodes.forEach((node) => {
if (typeof node === 'string') {
instructions.push(['PUSH', node])
} else {
const [input, topNode] = node
let child = topNode.topNode.firstChild
while (child) {
instructions.push(...this.#compileNode(child, input))
child = child.nextSibling
}
}
})
instructions.push(['STR_CONCAT', nodes.length])
return instructions
}
}

View File

@ -177,7 +177,20 @@ describe('curly strings', () => {
}`).toEvaluateTo("\n { one }\n two\n { three }\n ")
})
test("don't interpolate", () => {
expect(`{ sum is $(a + b)! }`).toEvaluateTo(` sum is $(a + b)! `)
test('interpolates variables', () => {
expect(`name = Bob; { Hello $name! }`).toEvaluateTo(` Hello Bob! `)
})
test("doesn't interpolate escaped variables ", () => {
expect(`name = Bob; { Hello \\$name }`).toEvaluateTo(` Hello $name `)
expect(`a = 1; b = 2; { sum is \\$(a + b)! }`).toEvaluateTo(` sum is $(a + b)! `)
})
test('interpolates expressions', () => {
expect(`a = 1; b = 2; { sum is $(a + b)! }`).toEvaluateTo(` sum is 3! `)
expect(`a = 1; b = 2; { sum is { $(a + b) }! }`).toEvaluateTo(` sum is { 3 }! `)
expect(`a = 1; b = 2; { sum is $(a + (b * b))! }`).toEvaluateTo(` sum is 5! `)
expect(`{ This is $({twisted}). }`).toEvaluateTo(` This is twisted. `)
expect(`{ This is $({{twisted}}). }`).toEvaluateTo(` This is {twisted}. `)
})
})

View File

@ -251,7 +251,9 @@ export const getStringParts = (node: SyntaxNode, input: string) => {
return (
child.type.id === terms.StringFragment ||
child.type.id === terms.Interpolation ||
child.type.id === terms.EscapeSeq
child.type.id === terms.EscapeSeq ||
child.type.id === terms.CurlyString
)
})
@ -260,7 +262,8 @@ export const getStringParts = (node: SyntaxNode, input: string) => {
if (
part.type.id !== terms.StringFragment &&
part.type.id !== terms.Interpolation &&
part.type.id !== terms.EscapeSeq
part.type.id !== terms.EscapeSeq &&
part.type.id !== terms.CurlyString
) {
throw new CompilerError(
`String child must be StringFragment, Interpolation, or EscapeSeq, got ${part.type.name}`,

View File

@ -0,0 +1,62 @@
import { parser } from '#parser/shrimp.ts'
import type { Tree } from '@lezer/common'
import { isIdentStart, isIdentChar } from './tokenizer'
// Turns a { curly string } into separate tokens for interpolation
export const tokenizeCurlyString = (value: string): (string | [string, Tree])[] => {
let pos = 1
let start = 1
let char = value[pos]
const tokens: (string | [string, Tree])[] = []
while (pos < value.length) {
if (char === '$') {
// escaped \$
if (value[pos - 1] === '\\' && value[pos - 2] !== '\\') {
tokens.push(value.slice(start, pos - 1))
start = pos
char = value[++pos]
continue
}
tokens.push(value.slice(start, pos))
start = pos
if (value[pos + 1] === '(') {
pos++ // slip opening '('
char = value[++pos]
if (!char) break
let depth = 0
while (char) {
if (char === '(') depth++
if (char === ')') depth--
if (depth < 0) break
char = value[++pos]
}
const input = value.slice(start + 2, pos) // skip '$('
tokens.push([input, parser.parse(input)])
start = ++pos // skip ')'
} else {
char = value[++pos]
if (!char) break
if (!isIdentStart(char.charCodeAt(0))) break
while (char && isIdentChar(char.charCodeAt(0)))
char = value[++pos]
const input = value.slice(start + 1, pos) // skip '$'
tokens.push([input, parser.parse(input)])
start = pos
}
}
char = value[++pos]
}
tokens.push(value.slice(start, pos - 1))
return tokens
}

View File

@ -37,7 +37,7 @@ finally { @specialize[@name=keyword]<Identifier, "finally"> }
throw { @specialize[@name=keyword]<Identifier, "throw"> }
null { @specialize[@name=Null]<Identifier, "null"> }
@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot, curlyString }
@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot, CurlyString }
@external specialize {Identifier} specializeKeyword from "./tokenizer" { Do }
@precedence {
@ -206,7 +206,7 @@ expression {
}
String {
"'" stringContent* "'" | curlyString
"'" stringContent* "'" | CurlyString
}
}

View File

@ -23,45 +23,45 @@ export const
AssignableIdentifier = 21,
Word = 22,
IdentifierBeforeDot = 23,
curlyString = 85,
Do = 24,
Comment = 25,
Program = 26,
PipeExpr = 27,
FunctionCall = 28,
DotGet = 29,
Number = 30,
ParenExpr = 31,
IfExpr = 32,
keyword = 70,
ConditionalOp = 34,
String = 35,
StringFragment = 36,
Interpolation = 37,
EscapeSeq = 38,
Boolean = 39,
Regex = 40,
Dict = 41,
NamedArg = 42,
NamedArgPrefix = 43,
FunctionDef = 44,
Params = 45,
NamedParam = 46,
Null = 47,
colon = 48,
CatchExpr = 49,
Block = 51,
FinallyExpr = 52,
Underscore = 55,
Array = 56,
ElseIfExpr = 57,
ElseExpr = 59,
FunctionCallOrIdentifier = 60,
BinOp = 61,
PositionalArg = 62,
WhileExpr = 64,
FunctionCallWithBlock = 66,
TryExpr = 67,
Throw = 69,
CompoundAssign = 71,
Assign = 72
CurlyString = 24,
Do = 25,
Comment = 26,
Program = 27,
PipeExpr = 28,
FunctionCall = 29,
DotGet = 30,
Number = 31,
ParenExpr = 32,
IfExpr = 33,
keyword = 71,
ConditionalOp = 35,
String = 36,
StringFragment = 37,
Interpolation = 38,
EscapeSeq = 39,
Boolean = 40,
Regex = 41,
Dict = 42,
NamedArg = 43,
NamedArgPrefix = 44,
FunctionDef = 45,
Params = 46,
NamedParam = 47,
Null = 48,
colon = 49,
CatchExpr = 50,
Block = 52,
FinallyExpr = 53,
Underscore = 56,
Array = 57,
ElseIfExpr = 58,
ElseExpr = 60,
FunctionCallOrIdentifier = 61,
BinOp = 62,
PositionalArg = 63,
WhileExpr = 65,
FunctionCallWithBlock = 67,
TryExpr = 68,
Throw = 70,
CompoundAssign = 72,
Assign = 73

View File

@ -4,24 +4,24 @@ import {operatorTokenizer} from "./operatorTokenizer"
import {tokenizer, specializeKeyword} from "./tokenizer"
import {trackScope} from "./scopeTracker"
import {highlighting} from "./highlight"
const spec_Identifier = {__proto__:null,if:66, null:94, catch:100, finally:106, end:108, else:116, while:130, try:136, throw:140}
const spec_Identifier = {__proto__:null,if:68, null:96, catch:102, finally:108, end:110, else:118, while:132, try:138, throw:142}
export const parser = LRParser.deserialize({
version: 14,
states: "9bQYQbOOO!gOSO'#DPOOQa'#DP'#DPOOQa'#DV'#DVO#sQbO'#DfO%XQcO'#E_OOQa'#E_'#E_O&bQcO'#E_O'dQcO'#E^O'zQcO'#E^O)jQRO'#DOO*yQcO'#EXO+TQcO'#EXO+eQbO'#C{O,cOpO'#CyOOQ`'#EY'#EYO,hQbO'#EXO,rQRO'#DuOOQ`'#EX'#EXO-WQQO'#EWOOQ`'#EW'#EWOOQ`'#Dw'#DwQYQbOOO-`QbO'#DYO-kQbO'#C|O.cQbO'#DnO/ZQQO'#DqO.cQbO'#DsO/`QbO'#DRO/hQWO'#DSOOOO'#Ea'#EaOOOO'#Dx'#DxO/|OSO,59kOOQa,59k,59kOOQ`'#Dy'#DyO0[QbO,5:QO0cQbO'#DWO0mQQO,59qOOQa,5:Q,5:QO0xQbO,5:QOOQa'#E^'#E^OOQ`'#Dl'#DlOOQ`'#Em'#EmOOQ`'#EQ'#EQO1SQbO,59dO1|QbO,5:bO.cQbO,59jO.cQbO,59jO.cQbO,59jO.cQbO,5:VO.cQbO,5:VO.cQbO,5:VO2^QRO,59gO2eQRO,59gO2pQRO,59gO2kQQO,59gO3RQQO,59gO3ZObO,59eO3fQbO'#ERO3qQbO,59cO4]QbO,5:[O1|QbO,5:aOOQ`,5:r,5:rOOQ`-E7u-E7uOOQ`'#Dz'#DzO4pQbO'#DZO4{QbO'#D[OOQO'#D{'#D{O4sQQO'#DZO5^QQO,59tO5cQcO'#E^O6wQRO'#E]O7OQRO'#E]OOQO'#E]'#E]O7ZQQO,59hO7`QRO,5:YO7gQRO,5:YO4]QbO,5:]O7rQcO,5:_O8nQcO,5:_O8xQcO,5:_OOOO,59m,59mOOOO,59n,59nOOOO-E7v-E7vOOQa1G/V1G/VOOQ`-E7w-E7wO9YQQO1G/]OOQa1G/l1G/lO9eQbO1G/lOOQ`,59r,59rOOQO'#D}'#D}O9YQQO1G/]OOQa1G/]1G/]OOQ`'#EO'#EOO9eQbO1G/lOOQ`-E8O-E8OOOQ`1G/|1G/|OOQa1G/U1G/UO:pQcO1G/UO:wQcO1G/UO;OQcO1G/UOOQa1G/q1G/qO;wQcO1G/qO<RQcO1G/qO<]QcO1G/qOOQa1G/R1G/ROOQa1G/P1G/PO=QQbO'#DjO=wQbO'#CxOOQ`,5:m,5:mOOQ`-E8P-E8POOQ`'#Da'#DaO>UQbO'#DaO>uQbO1G/vOOQ`1G/{1G/{OOQ`-E7x-E7xO?QQQO,59uOOQO,59v,59vOOQO-E7y-E7yO?YQbO1G/`O4]QbO1G/SO4]QbO1G/tO?mQbO1G/wO?xQQO7+$wOOQa7+$w7+$wO@TQbO7+%WOOQa7+%W7+%WOOQO-E7{-E7{OOQ`-E7|-E7|OOQ`'#D|'#D|O@_QQO'#D|O@dQbO'#EjOOQ`,59{,59{OATQbO'#D_OAYQQO'#DbOOQ`7+%b7+%bOA_QbO7+%bOAdQbO7+%bOAlQbO7+$zOAwQbO7+$zOBeQbO7+$nOBmQbO7+%`OOQ`7+%c7+%cOBrQbO7+%cOBwQbO7+%cOOQa<<Hc<<HcOOQa<<Hr<<HrOOQ`,5:h,5:hOOQ`-E7z-E7zOCPQQO,59yO4]QbO,59|OOQ`<<H|<<H|OCUQbO<<H|OOQ`<<Hf<<HfOCZQbO<<HfOC`QbO<<HfOChQbO<<HfOOQ`'#EP'#EPOCsQbO<<HYOC{QbO'#DiOOQ`<<HY<<HYODTQbO<<HYOOQ`<<Hz<<HzOOQ`<<H}<<H}ODYQbO<<H}O4]QbO1G/eOOQ`1G/h1G/hOOQ`AN>hAN>hOOQ`AN>QAN>QOD_QbOAN>QODdQbOAN>QOOQ`-E7}-E7}OOQ`AN=tAN=tODlQbOAN=tO-kQbO,5:RO4]QbO,5:TOOQ`AN>iAN>iOOQ`7+%P7+%POOQ`G23lG23lODqQbOG23lPDvQbO'#DgOOQ`G23`G23`OD{QQO1G/mOOQ`1G/o1G/oOOQ`LD)WLD)WO4]QbO7+%XOOQ`<<Hs<<Hs",
stateData: "ET~O!yOSiOS~OdXOeaOfUOg^OhgOnUOqhOwUOxUO!PUO!ciO!fjO!hkO!wQO#O]O#SPO#ZRO#[SO#]dO~OtnO#SqO#UlO#VmO~OdxOfUOg^OnUOwUOxUO{tO!PUO!wQO#O]O#SPO#ZRO#[SO#]rO~O#_vO~P!uOP#RXQ#RXR#RXS#RXT#RXU#RXW#RXX#RXY#RXZ#RX[#RX]#RX^#RX#]#RX#b#RX!S#RX!V#RX!W#RX![#RX~OdxOfUOg^OhgOnUOwUOxUO{tO!PUO!XyO!wQO#O]O#SPO#ZRO#[SO#`#RX!Q#RX~P#zOV}O~P#zOP#QXQ#QXR#QXS#QXT#QXU#QXW#QXX#QXY#QXZ#QX[#QX]#QX^#QX~O#]!{X#b!{X!S!{X!V!{X!W!{X![!{X~P&iOdxOfUOg^OhgOnUOwUOxUO{tO!PUO!XyO!wQO#O]O#SPO#ZRO#[SO!Q!^X!a!^X#]!^X#b!^X#`!^X!S!^X!V!^X!W!^X![!^X~P&iOP!SOQ!SOR!TOS!TOT!POU!QOW!OOX!OOY!OOZ!OO[!OO]!OO^!RO~O#]!{X#b!{X!S!{X!V!{X!W!{X![!{X~OT!POU!QO~P*eOP!SOQ!SOR!TOS!TO~P*eOdXOfUOg^OhgOnUOqhOwUOxUO!PUO!wQO#O]O#SPO#ZRO#[SO~O!}!ZO~O!Q!^O!a![O~P*eOV}O_!_O`!_Oa!_Ob!_Oc!_O~O#]!`O#b!`O~Od!bO{!dO!Q}P~Od!hOfUOg^OnUOwUOxUO!PUO!wQO#O]O#SPO#ZRO#[SO~OdxOfUOg^OnUOwUOxUO!PUO!wQO#O]O#SPO#ZRO#[SO~O!Q!oO~Od!sO#O]O~O#S!tO#U!tO#V!tO#W!tO#X!tO#Y!tO~OtnO#S!vO#UlO#VmO~O#_!yO~P!uOhgO!X!{O~P.cO{tO#]!|O#_#OO~O#]#PO#_!yO~P.cOhgO{tO!XyO!Qla!ala#]la#bla#`la!Sla!Vla!Wla![la~P.cOeaO!ciO!fjO!hkO~P+eO#`#]O~P&iOT!POU!QO#`#]O~OP!SOQ!SOR!TOS!TO#`#]O~O!a![O#`#]O~Od#^On#^O#O]O~Od#_Og^O#O]O~O!a![O#]ka#bka#`ka!Ska!Vka!Wka![ka~OeaO!ciO!fjO!hkO#]#dO~P+eOd!bO{!dO!Q}X~On#iOw#iO!P#iO!wQO#SPO~O!Q#kO~OhgO{tO!XyOT#QXU#QXW#QXX#QXY#QXZ#QX[#QX]#QX!Q#QX~P.cOT!POU!QOW!OOX!OOY!OOZ!OO[!OO]!OO~O!Q#PX~P6]OT!POU!QO!Q#PX~O!Q#lO~O!Q#mO~P6]OT!POU!QO!Q#mO~O#]!ga#b!ga!S!ga!V!ga!W!ga![!ga~P)jO#]!ga#b!ga!S!ga!V!ga!W!ga![!ga~OT!POU!QO~P8YOP!SOQ!SOR!TOS!TO~P8YO{tO#]!|O#_#pO~O#]#PO#_#rO~P.cOW!OOX!OOY!OOZ!OO[!OO]!OOTri#]ri#bri#`ri!Qri!Sri!Vri!Wri![ri~OU!QO~P9oOU!QO~P:ROUri~P9oO^!ROR!_iS!_i#]!_i#b!_i#`!_i!S!_i!V!_i!W!_i![!_i~OP!_iQ!_i~P;VOP!SOQ!SO~P;VOP!SOQ!SOR!_iS!_i#]!_i#b!_i#`!_i!S!_i!V!_i!W!_i![!_i~OhgO{tO!XyO!a!^X#]!^X#b!^X#`!^X!S!^X!V!^X!W!^X![!^X~P.cOhgO{tO!XyO~P.cOeaO!ciO!fjO!hkO#]#uO!S#^P!V#^P!W#^P![#^P~P+eO!S#yO!V#zO!W#{O~O{!dO!Q}a~OeaO!ciO!fjO!hkO#]$PO~P+eO!S#yO!V#zO!W$SO~O{tO#]!|O#_$VO~O#]#PO#_$WO~P.cO#]$XO~OeaO!ciO!fjO!hkO#]#uO!S#^X!V#^X!W#^X![#^X~P+eOd$ZO~O!Q$[O~O!W$]O~O!V#zO!W$]O~O!S#yO!V#zO!W$_O~OeaO!ciO!fjO!hkO#]#uO!S#^P!V#^P!W#^P~P+eO!W$fO![$eO~O!W$hO~O!W$iO~O!V#zO!W$iO~O!Q$kO~O!W$mO~O!W$nO~O!V#zO!W$nO~O!S#yO!V#zO!W$nO~O!W$rO![$eO~Oq$tO!Q$uO~O!W$rO~O!W$vO~O!W$xO~O!V#zO!W$xO~O!W${O~O!W%OO~Oq$tO~O!Q%PO~Onx~",
goto: "4y#bPPPPPPPPPPPPPPPPPPPPPPPPPPP#c#x$bP%e#cP&l'cP(b(bPP(f)bP)v*h*kPP*qP*}+gPPP+},{P-P-V-k.ZP.cP.c.cP.cP.c.c.u.{/R/X/_/i/p/z0U0[0fPPPP0m0q1_PP1w1}3gP4gPPPPPPPP4kPP4qpbOf}!^!_!o#d#k#l#m#w$P$[$k$u%PR!X]t_O]f}![!^!_!o#d#k#l#m#w$P$[$k$u%PT!kh$trXO]f}!^!_!o#d#k#l#m#w$P$[$k$u%PzxSTXikstw|!O!P!Q!R!S!T!h!z#Q#_#`#qS!hh$tR#_![vTO]fh}!^!_!o#d#k#l#m#w$P$[$k$t$u%PzUSTXikstw|!O!P!Q!R!S!T!h!z#Q#_#`#qQ!slQ#^!ZR#`![pZOf}!^!_!o#d#k#l#m#w$P$[$k$u%PQ!V]S!jh$tQ!niQ!qkQ#T!QR#V!P!rUOSTX]fhikstw|}!O!P!Q!R!S!T!^!_!h!o!z#Q#_#`#d#k#l#m#q#w$P$[$k$t$u%PR#i!dTnPp!sUOSTX]fhikstw|}!O!P!Q!R!S!T!^!_!h!o!z#Q#_#`#d#k#l#m#q#w$P$[$k$t$u%PQuS[zTX|!h#_#`Q!xsX!|u!x!}#opbOf}!^!_!o#d#k#l#m#w$P$[$k$u%P[yTX|!h#_#`Q!X]R!{tR!ggX!eg!c!f#hQ#}#eQ$U#nQ$a$OR$p$bQ#e!^Q#n!oQ$Q#lQ$R#mQ$l$[Q$w$kQ$}$uR%Q%PQ#|#eQ$T#nQ$^#}Q$`$OQ$j$US$o$a$bR$y$p!QUSTX]hikstw|!O!P!Q!R!S!T!h!z#Q#_#`#q$tqVOf}!^!_!o#d#k#l#m#w$P$[$k$u%PT$c$Q$dQ$g$QR$s$du_O]f}![!^!_!o#d#k#l#m#w$P$[$k$u%Pp[Of}!^!_!o#d#k#l#m#w$P$[$k$u%PQ!W]Q!rkQ#X!SR#[!T]zTX|!h#_#`qbOf}!^!_!o#d#k#l#m#w$P$[$k$u%PQfOR!afQpPR!upQsSR!wsQ!cgR#g!cQ!fgQ#h!cT#j!f#hS#w#d$PR$Y#wQ!}uQ#o!xT#s!}#oQ#QwQ#q!zT#t#Q#qQ$d$QR$q$dY|TX!h#_#`R#R|S!]`!YR#b!]TeOfScOfQ#S}`#c!^!o#l#m$[$k$u%PQ#f!_U#v#d#w$PR$O#kp`Of}!^!_!o#d#k#l#m#w$P$[$k$u%PQ!Y]R#a![Q!lhR$|$trYO]f}!^!_!o#d#k#l#m#w$P$[$k$u%PQwS[yTX|!h#_#`S!ih$tQ!miQ!pkQ!zsQ!{tW#Pw!z#Q#qQ#T!OQ#U!PQ#W!QQ#X!RQ#Y!SR#Z!TpWOf}!^!_!o#d#k#l#m#w$P$[$k$u%P!OxSTXhikstw|!O!P!Q!R!S!T!h!z#Q#_#`#q$tR!U]ToPpQ#x#dR$b$P]{TX|!h#_#`",
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo PlusEq MinusEq StarEq SlashEq ModuloEq Identifier AssignableIdentifier Word IdentifierBeforeDot Do Comment Program PipeExpr FunctionCall DotGet Number ParenExpr IfExpr keyword ConditionalOp String StringFragment Interpolation EscapeSeq Boolean Regex Dict NamedArg NamedArgPrefix FunctionDef Params NamedParam Null colon CatchExpr keyword Block FinallyExpr keyword keyword Underscore Array ElseIfExpr keyword ElseExpr FunctionCallOrIdentifier BinOp PositionalArg operator WhileExpr keyword FunctionCallWithBlock TryExpr keyword Throw keyword CompoundAssign Assign",
states: "9bQYQbOOO!gOSO'#DQOOQa'#DQ'#DQOOQa'#DW'#DWO#sQbO'#DgO%XQcO'#E_OOQa'#E_'#E_O&bQcO'#E_O'dQcO'#E^O'zQcO'#E^O)jQRO'#DPO*yQcO'#EXO+TQcO'#EXO+eQbO'#C|O,cOpO'#CzOOQ`'#EY'#EYO,hQbO'#EXO,rQRO'#DvOOQ`'#EX'#EXO-WQQO'#EWOOQ`'#EW'#EWOOQ`'#Dx'#DxQYQbOOO-`QbO'#DZO-kQbO'#C}O.cQbO'#DoO/ZQQO'#DrO.cQbO'#DtO/`QbO'#DSO/hQWO'#DTOOOO'#Ea'#EaOOOO'#Dy'#DyO/|OSO,59lOOQa,59l,59lOOQ`'#Dz'#DzO0[QbO,5:RO0cQbO'#DXO0mQQO,59rOOQa,5:R,5:RO0xQbO,5:ROOQa'#E^'#E^OOQ`'#Dm'#DmOOQ`'#Em'#EmOOQ`'#ER'#ERO1SQbO,59eO1|QbO,5:cO.cQbO,59kO.cQbO,59kO.cQbO,59kO.cQbO,5:WO.cQbO,5:WO.cQbO,5:WO2^QRO,59hO2eQRO,59hO2pQRO,59hO2kQQO,59hO3RQQO,59hO3ZObO,59fO3fQbO'#ESO3qQbO,59dO4]QbO,5:]O1|QbO,5:bOOQ`,5:r,5:rOOQ`-E7v-E7vOOQ`'#D{'#D{O4pQbO'#D[O4{QbO'#D]OOQO'#D|'#D|O4sQQO'#D[O5^QQO,59uO5cQcO'#E^O6wQRO'#E]O7OQRO'#E]OOQO'#E]'#E]O7ZQQO,59iO7`QRO,5:ZO7gQRO,5:ZO4]QbO,5:^O7rQcO,5:`O8nQcO,5:`O8xQcO,5:`OOOO,59n,59nOOOO,59o,59oOOOO-E7w-E7wOOQa1G/W1G/WOOQ`-E7x-E7xO9YQQO1G/^OOQa1G/m1G/mO9eQbO1G/mOOQ`,59s,59sOOQO'#EO'#EOO9YQQO1G/^OOQa1G/^1G/^OOQ`'#EP'#EPO9eQbO1G/mOOQ`-E8P-E8POOQ`1G/}1G/}OOQa1G/V1G/VO:pQcO1G/VO:wQcO1G/VO;OQcO1G/VOOQa1G/r1G/rO;wQcO1G/rO<RQcO1G/rO<]QcO1G/rOOQa1G/S1G/SOOQa1G/Q1G/QO=QQbO'#DkO=wQbO'#CyOOQ`,5:n,5:nOOQ`-E8Q-E8QOOQ`'#Db'#DbO>UQbO'#DbO>uQbO1G/wOOQ`1G/|1G/|OOQ`-E7y-E7yO?QQQO,59vOOQO,59w,59wOOQO-E7z-E7zO?YQbO1G/aO4]QbO1G/TO4]QbO1G/uO?mQbO1G/xO?xQQO7+$xOOQa7+$x7+$xO@TQbO7+%XOOQa7+%X7+%XOOQO-E7|-E7|OOQ`-E7}-E7}OOQ`'#D}'#D}O@_QQO'#D}O@dQbO'#EjOOQ`,59|,59|OATQbO'#D`OAYQQO'#DcOOQ`7+%c7+%cOA_QbO7+%cOAdQbO7+%cOAlQbO7+${OAwQbO7+${OBeQbO7+$oOBmQbO7+%aOOQ`7+%d7+%dOBrQbO7+%dOBwQbO7+%dOOQa<<Hd<<HdOOQa<<Hs<<HsOOQ`,5:i,5:iOOQ`-E7{-E7{OCPQQO,59zO4]QbO,59}OOQ`<<H}<<H}OCUQbO<<H}OOQ`<<Hg<<HgOCZQbO<<HgOC`QbO<<HgOChQbO<<HgOOQ`'#EQ'#EQOCsQbO<<HZOC{QbO'#DjOOQ`<<HZ<<HZODTQbO<<HZOOQ`<<H{<<H{OOQ`<<IO<<IOODYQbO<<IOO4]QbO1G/fOOQ`1G/i1G/iOOQ`AN>iAN>iOOQ`AN>RAN>ROD_QbOAN>RODdQbOAN>ROOQ`-E8O-E8OOOQ`AN=uAN=uODlQbOAN=uO-kQbO,5:SO4]QbO,5:UOOQ`AN>jAN>jOOQ`7+%Q7+%QOOQ`G23mG23mODqQbOG23mPDvQbO'#DhOOQ`G23aG23aOD{QQO1G/nOOQ`1G/p1G/pOOQ`LD)XLD)XO4]QbO7+%YOOQ`<<Ht<<Ht",
stateData: "ET~O!yOSjOS~OdXOeaOfUOg^OhQOigOoUOrhOxUOyUO!QUO!diO!gjO!ikO#O]O#SPO#ZRO#[SO#]dO~OunO#SqO#UlO#VmO~OdxOfUOg^OhQOoUOxUOyUO|tO!QUO#O]O#SPO#ZRO#[SO#]rO~O#_vO~P!uOP#RXQ#RXR#RXS#RXT#RXU#RXW#RXX#RXY#RXZ#RX[#RX]#RX^#RX#]#RX#b#RX!T#RX!W#RX!X#RX!]#RX~OdxOfUOg^OhQOigOoUOxUOyUO|tO!QUO!YyO#O]O#SPO#ZRO#[SO#`#RX!R#RX~P#zOV}O~P#zOP#QXQ#QXR#QXS#QXT#QXU#QXW#QXX#QXY#QXZ#QX[#QX]#QX^#QX~O#]!{X#b!{X!T!{X!W!{X!X!{X!]!{X~P&iOdxOfUOg^OhQOigOoUOxUOyUO|tO!QUO!YyO#O]O#SPO#ZRO#[SO!R!_X!b!_X#]!_X#b!_X#`!_X!T!_X!W!_X!X!_X!]!_X~P&iOP!SOQ!SOR!TOS!TOT!POU!QOW!OOX!OOY!OOZ!OO[!OO]!OO^!RO~O#]!{X#b!{X!T!{X!W!{X!X!{X!]!{X~OT!POU!QO~P*eOP!SOQ!SOR!TOS!TO~P*eOdXOfUOg^OhQOigOoUOrhOxUOyUO!QUO#O]O#SPO#ZRO#[SO~O!}!ZO~O!R!^O!b![O~P*eOV}O_!_O`!_Oa!_Ob!_Oc!_O~O#]!`O#b!`O~Od!bO|!dO!R!OP~Od!hOfUOg^OhQOoUOxUOyUO!QUO#O]O#SPO#ZRO#[SO~OdxOfUOg^OhQOoUOxUOyUO!QUO#O]O#SPO#ZRO#[SO~O!R!oO~Od!sO#O]O~O#S!tO#U!tO#V!tO#W!tO#X!tO#Y!tO~OunO#S!vO#UlO#VmO~O#_!yO~P!uOigO!Y!{O~P.cO|tO#]!|O#_#OO~O#]#PO#_!yO~P.cOigO|tO!YyO!Rma!bma#]ma#bma#`ma!Tma!Wma!Xma!]ma~P.cOeaO!diO!gjO!ikO~P+eO#`#]O~P&iOT!POU!QO#`#]O~OP!SOQ!SOR!TOS!TO#`#]O~O!b![O#`#]O~Od#^Oo#^O#O]O~Od#_Og^O#O]O~O!b![O#]la#bla#`la!Tla!Wla!Xla!]la~OeaO!diO!gjO!ikO#]#dO~P+eOd!bO|!dO!R!OX~OhQOo#iOx#iO!Q#iO#SPO~O!R#kO~OigO|tO!YyOT#QXU#QXW#QXX#QXY#QXZ#QX[#QX]#QX!R#QX~P.cOT!POU!QOW!OOX!OOY!OOZ!OO[!OO]!OO~O!R#PX~P6]OT!POU!QO!R#PX~O!R#lO~O!R#mO~P6]OT!POU!QO!R#mO~O#]!ha#b!ha!T!ha!W!ha!X!ha!]!ha~P)jO#]!ha#b!ha!T!ha!W!ha!X!ha!]!ha~OT!POU!QO~P8YOP!SOQ!SOR!TOS!TO~P8YO|tO#]!|O#_#pO~O#]#PO#_#rO~P.cOW!OOX!OOY!OOZ!OO[!OO]!OOTsi#]si#bsi#`si!Rsi!Tsi!Wsi!Xsi!]si~OU!QO~P9oOU!QO~P:ROUsi~P9oO^!ROR!`iS!`i#]!`i#b!`i#`!`i!T!`i!W!`i!X!`i!]!`i~OP!`iQ!`i~P;VOP!SOQ!SO~P;VOP!SOQ!SOR!`iS!`i#]!`i#b!`i#`!`i!T!`i!W!`i!X!`i!]!`i~OigO|tO!YyO!b!_X#]!_X#b!_X#`!_X!T!_X!W!_X!X!_X!]!_X~P.cOigO|tO!YyO~P.cOeaO!diO!gjO!ikO#]#uO!T#^P!W#^P!X#^P!]#^P~P+eO!T#yO!W#zO!X#{O~O|!dO!R!Oa~OeaO!diO!gjO!ikO#]$PO~P+eO!T#yO!W#zO!X$SO~O|tO#]!|O#_$VO~O#]#PO#_$WO~P.cO#]$XO~OeaO!diO!gjO!ikO#]#uO!T#^X!W#^X!X#^X!]#^X~P+eOd$ZO~O!R$[O~O!X$]O~O!W#zO!X$]O~O!T#yO!W#zO!X$_O~OeaO!diO!gjO!ikO#]#uO!T#^P!W#^P!X#^P~P+eO!X$fO!]$eO~O!X$hO~O!X$iO~O!W#zO!X$iO~O!R$kO~O!X$mO~O!X$nO~O!W#zO!X$nO~O!T#yO!W#zO!X$nO~O!X$rO!]$eO~Or$tO!R$uO~O!X$rO~O!X$vO~O!X$xO~O!W#zO!X$xO~O!X${O~O!X%OO~Or$tO~O!R%PO~Ooy~",
goto: "4y#bPPPPPPPPPPPPPPPPPPPPPPPPPPPP#c#x$bP%e#cP&l'cP(b(bPP(f)bP)v*h*kPP*qP*}+gPPP+},{P-P-V-k.ZP.cP.c.cP.cP.c.c.u.{/R/X/_/i/p/z0U0[0fPPP0m0q1_PP1w1}3gP4gPPPPPPPP4kPP4qpbOf}!^!_!o#d#k#l#m#w$P$[$k$u%PR!X]t_O]f}![!^!_!o#d#k#l#m#w$P$[$k$u%PT!kh$trXO]f}!^!_!o#d#k#l#m#w$P$[$k$u%PzxSTXikstw|!O!P!Q!R!S!T!h!z#Q#_#`#qS!hh$tR#_![vTO]fh}!^!_!o#d#k#l#m#w$P$[$k$t$u%PzUSTXikstw|!O!P!Q!R!S!T!h!z#Q#_#`#qQ!slQ#^!ZR#`![pZOf}!^!_!o#d#k#l#m#w$P$[$k$u%PQ!V]S!jh$tQ!niQ!qkQ#T!QR#V!P!rUOSTX]fhikstw|}!O!P!Q!R!S!T!^!_!h!o!z#Q#_#`#d#k#l#m#q#w$P$[$k$t$u%PR#i!dTnPp!sUOSTX]fhikstw|}!O!P!Q!R!S!T!^!_!h!o!z#Q#_#`#d#k#l#m#q#w$P$[$k$t$u%PQuS[zTX|!h#_#`Q!xsX!|u!x!}#opbOf}!^!_!o#d#k#l#m#w$P$[$k$u%P[yTX|!h#_#`Q!X]R!{tR!ggX!eg!c!f#hQ#}#eQ$U#nQ$a$OR$p$bQ#e!^Q#n!oQ$Q#lQ$R#mQ$l$[Q$w$kQ$}$uR%Q%PQ#|#eQ$T#nQ$^#}Q$`$OQ$j$US$o$a$bR$y$p!QUSTX]hikstw|!O!P!Q!R!S!T!h!z#Q#_#`#q$tqVOf}!^!_!o#d#k#l#m#w$P$[$k$u%PT$c$Q$dQ$g$QR$s$du_O]f}![!^!_!o#d#k#l#m#w$P$[$k$u%Pp[Of}!^!_!o#d#k#l#m#w$P$[$k$u%PQ!W]Q!rkQ#X!SR#[!T]zTX|!h#_#`qbOf}!^!_!o#d#k#l#m#w$P$[$k$u%PQfOR!afQpPR!upQsSR!wsQ!cgR#g!cQ!fgQ#h!cT#j!f#hS#w#d$PR$Y#wQ!}uQ#o!xT#s!}#oQ#QwQ#q!zT#t#Q#qQ$d$QR$q$dY|TX!h#_#`R#R|S!]`!YR#b!]TeOfScOfQ#S}`#c!^!o#l#m$[$k$u%PQ#f!_U#v#d#w$PR$O#kp`Of}!^!_!o#d#k#l#m#w$P$[$k$u%PQ!Y]R#a![Q!lhR$|$trYO]f}!^!_!o#d#k#l#m#w$P$[$k$u%PQwS[yTX|!h#_#`S!ih$tQ!miQ!pkQ!zsQ!{tW#Pw!z#Q#qQ#T!OQ#U!PQ#W!QQ#X!RQ#Y!SR#Z!TpWOf}!^!_!o#d#k#l#m#w$P$[$k$u%P!OxSTXhikstw|!O!P!Q!R!S!T!h!z#Q#_#`#q$tR!U]ToPpQ#x#dR$b$P]{TX|!h#_#`",
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo PlusEq MinusEq StarEq SlashEq ModuloEq Identifier AssignableIdentifier Word IdentifierBeforeDot CurlyString Do Comment Program PipeExpr FunctionCall DotGet Number ParenExpr IfExpr keyword ConditionalOp String StringFragment Interpolation EscapeSeq Boolean Regex Dict NamedArg NamedArgPrefix FunctionDef Params NamedParam Null colon CatchExpr keyword Block FinallyExpr keyword keyword Underscore Array ElseIfExpr keyword ElseExpr FunctionCallOrIdentifier BinOp PositionalArg operator WhileExpr keyword FunctionCallWithBlock TryExpr keyword Throw keyword CompoundAssign Assign",
maxTerm: 110,
context: trackScope,
nodeProps: [
["closedBy", 48,"end"]
["closedBy", 49,"end"]
],
propSources: [highlighting],
skippedNodes: [0,25],
skippedNodes: [0,26],
repeatNodeCount: 11,
tokenData: "C|~R|OX#{XY$jYZ%TZp#{pq$jqs#{st%ntu'tuw#{wx'yxy(Oyz(iz{#{{|)S|}#{}!O+v!O!P#{!P!Q.]!Q![)q![!]6x!]!^%T!^!}#{!}#O7c#O#P9X#P#Q9^#Q#R#{#R#S9w#S#T#{#T#Y,w#Y#Z:b#Z#b,w#b#c?`#c#f,w#f#g@]#g#h,w#h#iAY#i#o,w#o#p#{#p#qC^#q;'S#{;'S;=`$d<%l~#{~O#{~~CwS$QUtSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUtS!yYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UtS#]QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%sWtSOp#{pq&]qt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^&dZiYtSOY&]YZ#{Zt&]tu'Vuw&]wx'Vx#O&]#O#P'V#P;'S&];'S;=`'n<%lO&]Y'[SiYOY'VZ;'S'V;'S;=`'h<%lO'VY'kP;=`<%l'V^'qP;=`<%l&]~'yO#U~~(OO#S~U(VUtS#OQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(pUtS#`QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U)XWtSOt#{uw#{x!Q#{!Q![)q![#O#{#P;'S#{;'S;=`$d<%lO#{U)xYtSnQOt#{uw#{x!O#{!O!P*h!P!Q#{!Q![)q![#O#{#P;'S#{;'S;=`$d<%lO#{U*mWtSOt#{uw#{x!Q#{!Q![+V![#O#{#P;'S#{;'S;=`$d<%lO#{U+^WtSnQOt#{uw#{x!Q#{!Q![+V![#O#{#P;'S#{;'S;=`$d<%lO#{U+{^tSOt#{uw#{x}#{}!O,w!O!Q#{!Q![)q![!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{U,|[tSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{U-yU{QtSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U.bWtSOt#{uw#{x!P#{!P!Q.z!Q#O#{#P;'S#{;'S;=`$d<%lO#{U/P^tSOY/{YZ#{Zt/{tu1Ouw/{wx1Ox!P/{!P!Q#{!Q!}/{!}#O5q#O#P3^#P;'S/{;'S;=`6r<%lO/{U0S^tSxQOY/{YZ#{Zt/{tu1Ouw/{wx1Ox!P/{!P!Q3s!Q!}/{!}#O5q#O#P3^#P;'S/{;'S;=`6r<%lO/{Q1TXxQOY1OZ!P1O!P!Q1p!Q!}1O!}#O2_#O#P3^#P;'S1O;'S;=`3m<%lO1OQ1sP!P!Q1vQ1{UxQ#Z#[1v#]#^1v#a#b1v#g#h1v#i#j1v#m#n1vQ2bVOY2_Z#O2_#O#P2w#P#Q1O#Q;'S2_;'S;=`3W<%lO2_Q2zSOY2_Z;'S2_;'S;=`3W<%lO2_Q3ZP;=`<%l2_Q3aSOY1OZ;'S1O;'S;=`3m<%lO1OQ3pP;=`<%l1OU3xWtSOt#{uw#{x!P#{!P!Q4b!Q#O#{#P;'S#{;'S;=`$d<%lO#{U4ibtSxQOt#{uw#{x#O#{#P#Z#{#Z#[4b#[#]#{#]#^4b#^#a#{#a#b4b#b#g#{#g#h4b#h#i#{#i#j4b#j#m#{#m#n4b#n;'S#{;'S;=`$d<%lO#{U5v[tSOY5qYZ#{Zt5qtu2_uw5qwx2_x#O5q#O#P2w#P#Q/{#Q;'S5q;'S;=`6l<%lO5qU6oP;=`<%l5qU6uP;=`<%l/{U7PUtS!QQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7jW#[QtSOt#{uw#{x!_#{!_!`8S!`#O#{#P;'S#{;'S;=`$d<%lO#{U8XVtSOt#{uw#{x#O#{#P#Q8n#Q;'S#{;'S;=`$d<%lO#{U8uU#ZQtSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~9^O#V~U9eU#_QtSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U:OUtS!XQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U:g]tSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#U;`#U#o,w#o;'S#{;'S;=`$d<%lO#{U;e^tSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#`,w#`#a<a#a#o,w#o;'S#{;'S;=`$d<%lO#{U<f^tSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#g,w#g#h=b#h#o,w#o;'S#{;'S;=`$d<%lO#{U=g^tSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#X,w#X#Y>c#Y#o,w#o;'S#{;'S;=`$d<%lO#{U>j[wQtSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^?g[#WWtSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^@d[#YWtSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^Aa^#XWtSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#f,w#f#gB]#g#o,w#o;'S#{;'S;=`$d<%lO#{UBb^tSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#i,w#i#j=b#j#o,w#o;'S#{;'S;=`$d<%lO#{UCeU!aQtSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~C|O#b~",
tokenData: "C|~R|OX#{XY$jYZ%TZp#{pq$jqs#{st%ntu'tuw#{wx'yxy(Oyz(iz{#{{|)S|}#{}!O+v!O!P#{!P!Q.]!Q![)q![!]6x!]!^%T!^!}#{!}#O7c#O#P9X#P#Q9^#Q#R#{#R#S9w#S#T#{#T#Y,w#Y#Z:b#Z#b,w#b#c?`#c#f,w#f#g@]#g#h,w#h#iAY#i#o,w#o#p#{#p#qC^#q;'S#{;'S;=`$d<%l~#{~O#{~~CwS$QUuSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUuS!yYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UuS#]QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%sWuSOp#{pq&]qt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^&dZjYuSOY&]YZ#{Zt&]tu'Vuw&]wx'Vx#O&]#O#P'V#P;'S&];'S;=`'n<%lO&]Y'[SjYOY'VZ;'S'V;'S;=`'h<%lO'VY'kP;=`<%l'V^'qP;=`<%l&]~'yO#U~~(OO#S~U(VUuS#OQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(pUuS#`QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U)XWuSOt#{uw#{x!Q#{!Q![)q![#O#{#P;'S#{;'S;=`$d<%lO#{U)xYuSoQOt#{uw#{x!O#{!O!P*h!P!Q#{!Q![)q![#O#{#P;'S#{;'S;=`$d<%lO#{U*mWuSOt#{uw#{x!Q#{!Q![+V![#O#{#P;'S#{;'S;=`$d<%lO#{U+^WuSoQOt#{uw#{x!Q#{!Q![+V![#O#{#P;'S#{;'S;=`$d<%lO#{U+{^uSOt#{uw#{x}#{}!O,w!O!Q#{!Q![)q![!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{U,|[uSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{U-yU|QuSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U.bWuSOt#{uw#{x!P#{!P!Q.z!Q#O#{#P;'S#{;'S;=`$d<%lO#{U/P^uSOY/{YZ#{Zt/{tu1Ouw/{wx1Ox!P/{!P!Q#{!Q!}/{!}#O5q#O#P3^#P;'S/{;'S;=`6r<%lO/{U0S^uSyQOY/{YZ#{Zt/{tu1Ouw/{wx1Ox!P/{!P!Q3s!Q!}/{!}#O5q#O#P3^#P;'S/{;'S;=`6r<%lO/{Q1TXyQOY1OZ!P1O!P!Q1p!Q!}1O!}#O2_#O#P3^#P;'S1O;'S;=`3m<%lO1OQ1sP!P!Q1vQ1{UyQ#Z#[1v#]#^1v#a#b1v#g#h1v#i#j1v#m#n1vQ2bVOY2_Z#O2_#O#P2w#P#Q1O#Q;'S2_;'S;=`3W<%lO2_Q2zSOY2_Z;'S2_;'S;=`3W<%lO2_Q3ZP;=`<%l2_Q3aSOY1OZ;'S1O;'S;=`3m<%lO1OQ3pP;=`<%l1OU3xWuSOt#{uw#{x!P#{!P!Q4b!Q#O#{#P;'S#{;'S;=`$d<%lO#{U4ibuSyQOt#{uw#{x#O#{#P#Z#{#Z#[4b#[#]#{#]#^4b#^#a#{#a#b4b#b#g#{#g#h4b#h#i#{#i#j4b#j#m#{#m#n4b#n;'S#{;'S;=`$d<%lO#{U5v[uSOY5qYZ#{Zt5qtu2_uw5qwx2_x#O5q#O#P2w#P#Q/{#Q;'S5q;'S;=`6l<%lO5qU6oP;=`<%l5qU6uP;=`<%l/{U7PUuS!RQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7jW#[QuSOt#{uw#{x!_#{!_!`8S!`#O#{#P;'S#{;'S;=`$d<%lO#{U8XVuSOt#{uw#{x#O#{#P#Q8n#Q;'S#{;'S;=`$d<%lO#{U8uU#ZQuSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~9^O#V~U9eU#_QuSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U:OUuS!YQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U:g]uSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#U;`#U#o,w#o;'S#{;'S;=`$d<%lO#{U;e^uSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#`,w#`#a<a#a#o,w#o;'S#{;'S;=`$d<%lO#{U<f^uSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#g,w#g#h=b#h#o,w#o;'S#{;'S;=`$d<%lO#{U=g^uSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#X,w#X#Y>c#Y#o,w#o;'S#{;'S;=`$d<%lO#{U>j[xQuSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^?g[#WWuSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^@d[#YWuSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^Aa^#XWuSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#f,w#f#gB]#g#o,w#o;'S#{;'S;=`$d<%lO#{UBb^uSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#i,w#i#j=b#j#o,w#o;'S#{;'S;=`$d<%lO#{UCeU!bQuSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~C|O#b~",
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!}~~", 11)],
topRules: {"Program":[0,26]},
topRules: {"Program":[0,27]},
specialized: [{term: 20, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 20, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
tokenPrec: 1658
})

View File

@ -131,27 +131,30 @@ describe('string escape sequences', () => {
describe('curly strings', () => {
test('work on one line', () => {
expect('{ one two three }').toMatchTree(`
String one two three
String
CurlyString { one two three }
`)
})
test('work on multiple lines', () => {
expect(`{
expect(`{
one
two
three }`).toMatchTree(`
String
String
CurlyString {
one
two
three `)
three }`)
})
test('can contain other curlies', () => {
expect(`{ { one }
two
{ three } }`).toMatchTree(`
String { one }
String
CurlyString { { one }
two
{ three } `)
{ three } }`)
})
})

View File

@ -1,5 +1,5 @@
import { ExternalTokenizer, InputStream, Stack } from '@lezer/lr'
import { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot, Do, curlyString } from './shrimp.terms'
import { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot, Do, CurlyString } from './shrimp.terms'
// doobie doobie do (we need the `do` keyword to know when we're defining params)
export function specializeKeyword(ident: string) {
@ -20,9 +20,7 @@ export const tokenizer = new ExternalTokenizer(
const ch = getFullCodePoint(input, 0)
// Handle curly strings
if (ch === 123) { // {
return consumeCurlyString(input, stack)
}
if (ch === 123 /* { */) return consumeCurlyString(input, stack)
if (!isWordChar(ch)) return
@ -32,7 +30,7 @@ export const tokenizer = new ExternalTokenizer(
// 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) || isEmojiOrUnicode(ch)
const isValidStart = isIdentStart(ch)
const canBeWord = stack.canShift(Word)
// Consume all word characters, tracking if it remains a valid identifier
@ -125,7 +123,7 @@ const consumeWordToken = (
}
// Track identifier validity: must be lowercase, digit, dash, or emoji/unicode
if (!isLowercaseLetter(ch) && !isDigit(ch) && ch !== 45 /* - */ && ch !== 63 /* ? */ && !isEmojiOrUnicode(ch)) {
if (!isIdentChar(ch)) {
if (!canBeWord) break
isValidIdentifier = false
}
@ -157,8 +155,9 @@ const consumeRestOfWord = (input: InputStream, startPos: number, canBeWord: bool
return pos
}
// Consumes { curly strings } and tracks braces so you can { have { braces { inside { braces } } }
const consumeCurlyString = (input: InputStream, stack: Stack) => {
if (!stack.canShift(curlyString)) return
if (!stack.canShift(CurlyString)) return
let depth = 0
let pos = 0
@ -179,7 +178,7 @@ const consumeCurlyString = (input: InputStream, stack: Stack) => {
pos++
}
input.acceptToken(curlyString, pos)
input.acceptToken(CurlyString, pos)
}
// Check if this identifier is in scope (for property access detection)
@ -239,6 +238,14 @@ const chooseIdentifierToken = (input: InputStream, stack: Stack): number => {
}
// Character classification helpers
export const isIdentStart = (ch: number): boolean => {
return isLowercaseLetter(ch) || isEmojiOrUnicode(ch)
}
export const isIdentChar = (ch: number): boolean => {
return isLowercaseLetter(ch) || isDigit(ch) || ch === 45 /* - */ || ch === 63 /* ? */ || isEmojiOrUnicode(ch)
}
const isWhiteSpace = (ch: number): boolean => {
return ch === 32 /* space */ || ch === 9 /* tab */ || ch === 13 /* \r */
}