Compare commits

...

7 Commits

Author SHA1 Message Date
c273429b24 "add double quoted strings" 2025-11-07 22:03:04 -08:00
c127566abe topNode.topNode 2025-11-06 21:30:50 -08:00
b7a65e07dc fix test issues 2025-11-06 21:26:37 -08:00
8299022b4f fix edge case 2025-11-06 21:24:05 -08:00
131c943fc6 interpolation in { curly strings } 2025-11-06 21:24:03 -08:00
866da86862 curly -> Curly 2025-11-06 21:23:31 -08:00
5ac0b02044 { curly strings } 2025-11-06 21:23:31 -08:00
11 changed files with 312 additions and 69 deletions

View File

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

View File

@ -155,3 +155,69 @@ describe('dict literals', () => {
c=3]`).toEvaluateTo({ a: 1, b: 2, c: 3 }) c=3]`).toEvaluateTo({ a: 1, b: 2, c: 3 })
}) })
}) })
describe('curly strings', () => {
test('work on one line', () => {
expect('{ one two three }').toEvaluateTo(" one two three ")
})
test('work on multiple lines', () => {
expect(`{
one
two
three
}`).toEvaluateTo("\n one\n two\n three\n ")
})
test('can contain other curlies', () => {
expect(`{
{ one }
two
{ three }
}`).toEvaluateTo("\n { one }\n two\n { three }\n ")
})
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}. `)
})
test('interpolation edge cases', () => {
expect(`{[a=1 b=2 c={wild}]}`).toEvaluateTo(`[a=1 b=2 c={wild}]`)
expect(`a = 1;b = 2;c = 3;{$a $b $c}`).toEvaluateTo(`1 2 3`)
expect(`a = 1;b = 2;c = 3;{$a$b$c}`).toEvaluateTo(`123`)
})
})
describe('double quoted strings', () => {
test("work", () => {
expect(`"hello world"`).toEvaluateTo('hello world')
})
test("don't interpolate", () => {
expect(`"hello $world"`).toEvaluateTo('hello $world')
expect(`"hello $(1 + 2)"`).toEvaluateTo('hello $(1 + 2)')
})
test("equal regular strings", () => {
expect(`"hello world" == 'hello world'`).toEvaluateTo(true)
})
test("can contain newlines", () => {
expect(`
"hello
world"`).toEvaluateTo('hello\n world')
})
})

View File

@ -89,7 +89,7 @@ describe('pipe expressions', () => {
test('pipe with prelude function (echo)', () => { test('pipe with prelude function (echo)', () => {
expect(` expect(`
get-msg = do: 'hello' end get-msg = do: 'hello' end
get-msg | echo get-msg | length
`).toEvaluateTo(null) `).toEvaluateTo(5)
}) })
}) })

View File

@ -83,7 +83,7 @@ end
test('custom tags', () => { test('custom tags', () => {
expect(` expect(`
list = tag ul class=list list = tag ul class='list'
ribbit: ribbit:
list: list:
li border-bottom='1px solid black' one li border-bottom='1px solid black' one

View File

@ -251,7 +251,9 @@ export const getStringParts = (node: SyntaxNode, input: string) => {
return ( return (
child.type.id === terms.StringFragment || child.type.id === terms.StringFragment ||
child.type.id === terms.Interpolation || 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 ( if (
part.type.id !== terms.StringFragment && part.type.id !== terms.StringFragment &&
part.type.id !== terms.Interpolation && part.type.id !== terms.Interpolation &&
part.type.id !== terms.EscapeSeq part.type.id !== terms.EscapeSeq &&
part.type.id !== terms.CurlyString
) { ) {
throw new CompilerError( throw new CompilerError(
`String child must be StringFragment, Interpolation, or EscapeSeq, got ${part.type.name}`, `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 { SyntaxNode } from '@lezer/common'
import { isIdentStart, isIdentChar } from './tokenizer'
// Turns a { curly string } into strings and nodes for interpolation
export const tokenizeCurlyString = (value: string): (string | [string, SyntaxNode])[] => {
let pos = 1
let start = 1
let char = value[pos]
const tokens: (string | [string, SyntaxNode])[] = []
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).topNode])
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).topNode])
start = pos-- // backtrack and start over
}
}
char = value[++pos]
}
tokens.push(value.slice(start, pos - 1))
return tokens
}

View File

@ -12,6 +12,7 @@
@precedence { Number Regex } @precedence { Number Regex }
StringFragment { !['\\$]+ } StringFragment { !['\\$]+ }
DoubleQuote { '"' !["]* '"' }
NamedArgPrefix { $[a-z-]+ "=" } NamedArgPrefix { $[a-z-]+ "=" }
Number { ("-" | "+")? $[0-9]+ ('.' $[0-9]+)? } Number { ("-" | "+")? $[0-9]+ ('.' $[0-9]+)? }
Boolean { "true" | "false" } Boolean { "true" | "false" }
@ -37,7 +38,7 @@ finally { @specialize[@name=keyword]<Identifier, "finally"> }
throw { @specialize[@name=keyword]<Identifier, "throw"> } throw { @specialize[@name=keyword]<Identifier, "throw"> }
null { @specialize[@name=Null]<Identifier, "null"> } null { @specialize[@name=Null]<Identifier, "null"> }
@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot } @external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot, CurlyString }
@external specialize {Identifier} specializeKeyword from "./tokenizer" { Do } @external specialize {Identifier} specializeKeyword from "./tokenizer" { Do }
@precedence { @precedence {
@ -205,7 +206,9 @@ expression {
IdentifierBeforeDot dot (Number | Identifier | ParenExpr) IdentifierBeforeDot dot (Number | Identifier | ParenExpr)
} }
String { "'" stringContent* "'" } String {
"'" stringContent* "'" | CurlyString | DoubleQuote
}
} }
stringContent { stringContent {

View File

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

View File

@ -4,24 +4,24 @@ import {operatorTokenizer} from "./operatorTokenizer"
import {tokenizer, specializeKeyword} from "./tokenizer" import {tokenizer, specializeKeyword} from "./tokenizer"
import {trackScope} from "./parserScopeContext" import {trackScope} from "./parserScopeContext"
import {highlighting} from "./highlight" 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:98, catch:104, finally:110, end:112, else:120, while:134, try:140, throw:144}
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, version: 14,
states: "9[QYQbOOO!dOSO'#DPOOQa'#DV'#DVO#mQbO'#DfO%RQcO'#E^OOQa'#E^'#E^O&XQcO'#E^O'ZQcO'#E]O'qQcO'#E]O)^QRO'#DOO*mQcO'#EWO*wQcO'#EWO+XQbO'#C{O,SOpO'#CyOOQ`'#EX'#EXO,XQbO'#EWO,cQRO'#DuOOQ`'#EW'#EWO,wQQO'#EVOOQ`'#EV'#EVOOQ`'#Dw'#DwQYQbOOO-PQbO'#DYO-[QbO'#C|O.PQbO'#DnO.tQQO'#DqO.PQbO'#DsO.yQbO'#DRO/RQWO'#DSOOOO'#E`'#E`OOOO'#Dx'#DxO/gOSO,59kOOQa,59k,59kOOQ`'#Dy'#DyO/uQbO,5:QO/|QbO'#DWO0WQQO,59qOOQa,5:Q,5:QO0cQbO,5:QOOQa'#E]'#E]OOQ`'#Dl'#DlOOQ`'#El'#ElOOQ`'#EQ'#EQO0mQbO,59dO1gQbO,5:bO.PQbO,59jO.PQbO,59jO.PQbO,59jO.PQbO,5:VO.PQbO,5:VO.PQbO,5:VO1wQRO,59gO2OQRO,59gO2ZQRO,59gO2UQQO,59gO2lQQO,59gO2tObO,59eO3PQbO'#ERO3[QbO,59cO3vQbO,5:[O1gQbO,5:aOOQ`,5:q,5:qOOQ`-E7u-E7uOOQ`'#Dz'#DzO4ZQbO'#DZO4fQbO'#D[OOQO'#D{'#D{O4^QQO'#DZO4tQQO,59tO4yQcO'#E]O6_QRO'#E[O6fQRO'#E[OOQO'#E['#E[O6qQQO,59hO6vQRO,5:YO6}QRO,5:YO3vQbO,5:]O7YQcO,5:_O8UQcO,5:_O8`QcO,5:_OOOO,59m,59mOOOO,59n,59nOOOO-E7v-E7vOOQa1G/V1G/VOOQ`-E7w-E7wO8pQQO1G/]OOQa1G/l1G/lO8{QbO1G/lOOQ`,59r,59rOOQO'#D}'#D}O8pQQO1G/]OOQa1G/]1G/]OOQ`'#EO'#EOO8{QbO1G/lOOQ`-E8O-E8OOOQ`1G/|1G/|OOQa1G/U1G/UO:WQcO1G/UO:_QcO1G/UO:fQcO1G/UOOQa1G/q1G/qO;_QcO1G/qO;iQcO1G/qO;sQcO1G/qOOQa1G/R1G/ROOQa1G/P1G/PO<hQbO'#DjO=_QbO'#CxOOQ`,5:m,5:mOOQ`-E8P-E8POOQ`'#Da'#DaO=lQbO'#DaO>]QbO1G/vOOQ`1G/{1G/{OOQ`-E7x-E7xO>hQQO,59uOOQO,59v,59vOOQO-E7y-E7yO>pQbO1G/`O3vQbO1G/SO3vQbO1G/tO?TQbO1G/wO?`QQO7+$wOOQa7+$w7+$wO?kQbO7+%WOOQa7+%W7+%WOOQO-E7{-E7{OOQ`-E7|-E7|OOQ`'#D|'#D|O?uQQO'#D|O?zQbO'#EiOOQ`,59{,59{O@kQbO'#D_O@pQQO'#DbOOQ`7+%b7+%bO@uQbO7+%bO@zQbO7+%bOASQbO7+$zOA_QbO7+$zOA{QbO7+$nOBTQbO7+%`OOQ`7+%c7+%cOBYQbO7+%cOB_QbO7+%cOOQa<<Hc<<HcOOQa<<Hr<<HrOOQ`,5:h,5:hOOQ`-E7z-E7zOBgQQO,59yO3vQbO,59|OOQ`<<H|<<H|OBlQbO<<H|OOQ`<<Hf<<HfOBqQbO<<HfOBvQbO<<HfOCOQbO<<HfOOQ`'#EP'#EPOCZQbO<<HYOCcQbO'#DiOOQ`<<HY<<HYOCkQbO<<HYOOQ`<<Hz<<HzOOQ`<<H}<<H}OCpQbO<<H}O3vQbO1G/eOOQ`1G/h1G/hOOQ`AN>hAN>hOOQ`AN>QAN>QOCuQbOAN>QOCzQbOAN>QOOQ`-E7}-E7}OOQ`AN=tAN=tODSQbOAN=tO-[QbO,5:RO3vQbO,5:TOOQ`AN>iAN>iOOQ`7+%P7+%POOQ`G23lG23lODXQbOG23lPD^QbO'#DgOOQ`G23`G23`ODcQQO1G/mOOQ`1G/o1G/oOOQ`LD)WLD)WO3vQbO7+%XOOQ`<<Hs<<Hs", states: "9bQYQbOOO!jOSO'#DQOOQa'#DQ'#DQOOQa'#DX'#DXO#yQbO'#DhO%_QcO'#E`OOQa'#E`'#E`O&kQcO'#E`O'mQcO'#E_O(TQcO'#E_O)vQRO'#DPO+VQcO'#EYO+aQcO'#EYO+qQbO'#C|O,rOpO'#CzOOQ`'#EZ'#EZO,wQbO'#EYO-RQRO'#DwOOQ`'#EY'#EYO-gQQO'#EXOOQ`'#EX'#EXOOQ`'#Dy'#DyQYQbOOO-oQbO'#D[O-zQbO'#C}O.uQbO'#DpO/pQQO'#DsO.uQbO'#DuO/uQbO'#DSO/}QWO'#DTOOOO'#Eb'#EbOOOO'#Dz'#DzO0cOSO,59lOOQa,59l,59lOOQ`'#D{'#D{O0qQbO,5:SO0xQbO'#DYO1SQQO,59sOOQa,5:S,5:SO1_QbO,5:SOOQa'#E_'#E_OOQ`'#Dn'#DnOOQ`'#En'#EnOOQ`'#ES'#ESO1iQbO,59eO2cQbO,5:dO.uQbO,59kO.uQbO,59kO.uQbO,59kO.uQbO,5:XO.uQbO,5:XO.uQbO,5:XO2sQRO,59hO2zQRO,59hO3VQRO,59hO3QQQO,59hO3hQQO,59hO3pObO,59fO3{QbO'#ETO4WQbO,59dO4rQbO,5:^O2cQbO,5:cOOQ`,5:s,5:sOOQ`-E7w-E7wOOQ`'#D|'#D|O5VQbO'#D]O5bQbO'#D^OOQO'#D}'#D}O5YQQO'#D]O5vQQO,59vO5{QcO'#E_O7aQRO'#E^O7hQRO'#E^OOQO'#E^'#E^O7sQQO,59iO7xQRO,5:[O8PQRO,5:[O4rQbO,5:_O8[QcO,5:aO9WQcO,5:aO9bQcO,5:aOOOO,59n,59nOOOO,59o,59oOOOO-E7x-E7xOOQa1G/W1G/WOOQ`-E7y-E7yO9rQQO1G/_OOQa1G/n1G/nO9}QbO1G/nOOQ`,59t,59tOOQO'#EP'#EPO9rQQO1G/_OOQa1G/_1G/_OOQ`'#EQ'#EQO9}QbO1G/nOOQ`-E8Q-E8QOOQ`1G0O1G0OOOQa1G/V1G/VO;YQcO1G/VO;aQcO1G/VO;hQcO1G/VOOQa1G/s1G/sO<aQcO1G/sO<kQcO1G/sO<uQcO1G/sOOQa1G/S1G/SOOQa1G/Q1G/QO=jQbO'#DlO>aQbO'#CyOOQ`,5:o,5:oOOQ`-E8R-E8ROOQ`'#Dc'#DcO>nQbO'#DcO?_QbO1G/xOOQ`1G/}1G/}OOQ`-E7z-E7zO?jQQO,59wOOQO,59x,59xOOQO-E7{-E7{O?rQbO1G/bO4rQbO1G/TO4rQbO1G/vO@VQbO1G/yO@bQQO7+$yOOQa7+$y7+$yO@mQbO7+%YOOQa7+%Y7+%YOOQO-E7}-E7}OOQ`-E8O-E8OOOQ`'#EO'#EOO@wQQO'#EOO@|QbO'#EkOOQ`,59},59}OAmQbO'#DaOArQQO'#DdOOQ`7+%d7+%dOAwQbO7+%dOA|QbO7+%dOBUQbO7+$|OBaQbO7+$|OB}QbO7+$oOCVQbO7+%bOOQ`7+%e7+%eOC[QbO7+%eOCaQbO7+%eOOQa<<He<<HeOOQa<<Ht<<HtOOQ`,5:j,5:jOOQ`-E7|-E7|OCiQQO,59{O4rQbO,5:OOOQ`<<IO<<IOOCnQbO<<IOOOQ`<<Hh<<HhOCsQbO<<HhOCxQbO<<HhODQQbO<<HhOOQ`'#ER'#EROD]QbO<<HZODeQbO'#DkOOQ`<<HZ<<HZODmQbO<<HZOOQ`<<H|<<H|OOQ`<<IP<<IPODrQbO<<IPO4rQbO1G/gOOQ`1G/j1G/jOOQ`AN>jAN>jOOQ`AN>SAN>SODwQbOAN>SOD|QbOAN>SOOQ`-E8P-E8POOQ`AN=uAN=uOEUQbOAN=uO-zQbO,5:TO4rQbO,5:VOOQ`AN>kAN>kOOQ`7+%R7+%ROOQ`G23nG23nOEZQbOG23nPE`QbO'#DiOOQ`G23aG23aOEeQQO1G/oOOQ`1G/q1G/qOOQ`LD)YLD)YO4rQbO7+%ZOOQ`<<Hu<<Hu",
stateData: "Dk~O!xOSiOS~OdWOe`OfTOg]OhfOnTOqgOwTOxTO!PTO!chO!fiO!hjO!}[O#RPO#YQO#ZRO#[cO~OtmO#RpO#TkO#UlO~OdwOfTOg]OnTOwTOxTO{sO!PTO!}[O#RPO#YQO#ZRO#[qO~O#^uO~P!rOP#QXQ#QXR#QXS#QXT#QXU#QXW#QXX#QXY#QXZ#QX[#QX]#QX^#QX#[#QX#a#QX!S#QX!V#QX!W#QX![#QX~OdwOfTOg]OhfOnTOwTOxTO{sO!PTO!XxO!}[O#RPO#YQO#ZRO#_#QX!Q#QX~P#tOV|O~P#tOP#PXQ#PXR#PXS#PXT#PXU#PXW#PXX#PXY#PXZ#PX[#PX]#PX^#PX~O#[!zX#a!zX!S!zX!V!zX!W!zX![!zX~P&`OdwOfTOg]OhfOnTOwTOxTO{sO!PTO!XxO!}[O#RPO#YQO#ZRO!Q!^X!a!^X#[!^X#a!^X#_!^X!S!^X!V!^X!W!^X![!^X~P&`OP!ROQ!ROR!SOS!SOT!OOU!POW}OX}OY}OZ}O[}O]}O^!QO~O#[!zX#a!zX!S!zX!V!zX!W!zX![!zX~OT!OOU!PO~P*XOP!ROQ!ROR!SOS!SO~P*XOdWOfTOg]OhfOnTOqgOwTOxTO!PTO!}[O#RPO#YQO#ZRO~O!|!YO~O!Q!]O!a!ZO~P*XOV|O_!^O`!^Oa!^Ob!^Oc!^O~O#[!_O#a!_O~Od!aO{!cO!Q}P~Od!gOfTOg]OnTOwTOxTO!PTO!}[O#RPO#YQO#ZRO~OdwOfTOg]OnTOwTOxTO!PTO!}[O#RPO#YQO#ZRO~O!Q!nO~Od!rO!}[O~O#R!sO#T!sO#U!sO#V!sO#W!sO#X!sO~OtmO#R!uO#TkO#UlO~O#^!xO~P!rOhfO!X!zO~P.PO{sO#[!{O#^!}O~O#[#OO#^!xO~P.POhfO{sO!XxO!Qla!ala#[la#ala#_la!Sla!Vla!Wla![la~P.POe`O!chO!fiO!hjO~P+XO#_#[O~P&`OT!OOU!PO#_#[O~OP!ROQ!ROR!SOS!SO#_#[O~O!a!ZO#_#[O~Od#]On#]O!}[O~Od#^Og]O!}[O~O!a!ZO#[ka#aka#_ka!Ska!Vka!Wka![ka~Oe`O!chO!fiO!hjO#[#cO~P+XOd!aO{!cO!Q}X~On#hOw#hO!P#hO#RPO~O!Q#jO~OhfO{sO!XxOT#PXU#PXW#PXX#PXY#PXZ#PX[#PX]#PX!Q#PX~P.POT!OOU!POW}OX}OY}OZ}O[}O]}O~O!Q#OX~P5sOT!OOU!PO!Q#OX~O!Q#kO~O!Q#lO~P5sOT!OOU!PO!Q#lO~O#[!ga#a!ga!S!ga!V!ga!W!ga![!ga~P)^O#[!ga#a!ga!S!ga!V!ga!W!ga![!ga~OT!OOU!PO~P7pOP!ROQ!ROR!SOS!SO~P7pO{sO#[!{O#^#oO~O#[#OO#^#qO~P.POW}OX}OY}OZ}O[}O]}OTri#[ri#ari#_ri!Qri!Sri!Vri!Wri![ri~OU!PO~P9VOU!PO~P9iOUri~P9VO^!QOR!_iS!_i#[!_i#a!_i#_!_i!S!_i!V!_i!W!_i![!_i~OP!_iQ!_i~P:mOP!ROQ!RO~P:mOP!ROQ!ROR!_iS!_i#[!_i#a!_i#_!_i!S!_i!V!_i!W!_i![!_i~OhfO{sO!XxO!a!^X#[!^X#a!^X#_!^X!S!^X!V!^X!W!^X![!^X~P.POhfO{sO!XxO~P.POe`O!chO!fiO!hjO#[#tO!S#]P!V#]P!W#]P![#]P~P+XO!S#xO!V#yO!W#zO~O{!cO!Q}a~Oe`O!chO!fiO!hjO#[$OO~P+XO!S#xO!V#yO!W$RO~O{sO#[!{O#^$UO~O#[#OO#^$VO~P.PO#[$WO~Oe`O!chO!fiO!hjO#[#tO!S#]X!V#]X!W#]X![#]X~P+XOd$YO~O!Q$ZO~O!W$[O~O!V#yO!W$[O~O!S#xO!V#yO!W$^O~Oe`O!chO!fiO!hjO#[#tO!S#]P!V#]P!W#]P~P+XO!W$eO![$dO~O!W$gO~O!W$hO~O!V#yO!W$hO~O!Q$jO~O!W$lO~O!W$mO~O!V#yO!W$mO~O!S#xO!V#yO!W$mO~O!W$qO![$dO~Oq$sO!Q$tO~O!W$qO~O!W$uO~O!W$wO~O!V#yO!W$wO~O!W$zO~O!W$}O~Oq$sO~O!Q%OO~Onx~", stateData: "Em~O!zOSjOS~OdXOeaOfUOg^OhQOigOoUOrhOxQOyUOzUO!RUO!eiO!hjO!jkO#P]O#TPO#[RO#]SO#^dO~OunO#TqO#VlO#WmO~OdxOfUOg^OhQOoUOxQOyUOzUO}tO!RUO#P]O#TPO#[RO#]SO#^rO~O#`vO~P!xOP#SXQ#SXR#SXS#SXT#SXU#SXW#SXX#SXY#SXZ#SX[#SX]#SX^#SX#^#SX#c#SX!U#SX!X#SX!Y#SX!^#SX~OdxOfUOg^OhQOigOoUOxQOyUOzUO}tO!RUO!ZyO#P]O#TPO#[RO#]SO#a#SX!S#SX~P$QOV}O~P$QOP#RXQ#RXR#RXS#RXT#RXU#RXW#RXX#RXY#RXZ#RX[#RX]#RX^#RX~O#^!|X#c!|X!U!|X!X!|X!Y!|X!^!|X~P&rOdxOfUOg^OhQOigOoUOxQOyUOzUO}tO!RUO!ZyO#P]O#TPO#[RO#]SO!S!`X!c!`X#^!`X#c!`X#a!`X!U!`X!X!`X!Y!`X!^!`X~P&rOP!SOQ!SOR!TOS!TOT!POU!QOW!OOX!OOY!OOZ!OO[!OO]!OO^!RO~O#^!|X#c!|X!U!|X!X!|X!Y!|X!^!|X~OT!POU!QO~P*qOP!SOQ!SOR!TOS!TO~P*qOdXOfUOg^OhQOigOoUOrhOxQOyUOzUO!RUO#P]O#TPO#[RO#]SO~O#O!ZO~O!S!^O!c![O~P*qOV}O_!_O`!_Oa!_Ob!_Oc!_O~O#^!`O#c!`O~Od!bO}!dO!S!PP~Od!hOfUOg^OhQOoUOxQOyUOzUO!RUO#P]O#TPO#[RO#]SO~OdxOfUOg^OhQOoUOxQOyUOzUO!RUO#P]O#TPO#[RO#]SO~O!S!oO~Od!sO#P]O~O#T!tO#V!tO#W!tO#X!tO#Y!tO#Z!tO~OunO#T!vO#VlO#WmO~O#`!yO~P!xOigO!Z!{O~P.uO}tO#^!|O#`#OO~O#^#PO#`!yO~P.uOigO}tO!ZyO!Sma!cma#^ma#cma#ama!Uma!Xma!Yma!^ma~P.uOeaO!eiO!hjO!jkO~P+qO#a#]O~P&rOT!POU!QO#a#]O~OP!SOQ!SOR!TOS!TO#a#]O~O!c![O#a#]O~Od#^Oo#^O#P]O~Od#_Og^O#P]O~O!c![O#^la#cla#ala!Ula!Xla!Yla!^la~OeaO!eiO!hjO!jkO#^#dO~P+qOd!bO}!dO!S!PX~OhQOo#iOxQOy#iO!R#iO#TPO~O!S#kO~OigO}tO!ZyOT#RXU#RXW#RXX#RXY#RXZ#RX[#RX]#RX!S#RX~P.uOT!POU!QOW!OOX!OOY!OOZ!OO[!OO]!OO~O!S#QX~P6uOT!POU!QO!S#QX~O!S#lO~O!S#mO~P6uOT!POU!QO!S#mO~O#^!ia#c!ia!U!ia!X!ia!Y!ia!^!ia~P)vO#^!ia#c!ia!U!ia!X!ia!Y!ia!^!ia~OT!POU!QO~P8rOP!SOQ!SOR!TOS!TO~P8rO}tO#^!|O#`#pO~O#^#PO#`#rO~P.uOW!OOX!OOY!OOZ!OO[!OO]!OOTsi#^si#csi#asi!Ssi!Usi!Xsi!Ysi!^si~OU!QO~P:XOU!QO~P:kOUsi~P:XO^!ROR!aiS!ai#^!ai#c!ai#a!ai!U!ai!X!ai!Y!ai!^!ai~OP!aiQ!ai~P;oOP!SOQ!SO~P;oOP!SOQ!SOR!aiS!ai#^!ai#c!ai#a!ai!U!ai!X!ai!Y!ai!^!ai~OigO}tO!ZyO!c!`X#^!`X#c!`X#a!`X!U!`X!X!`X!Y!`X!^!`X~P.uOigO}tO!ZyO~P.uOeaO!eiO!hjO!jkO#^#uO!U#_P!X#_P!Y#_P!^#_P~P+qO!U#yO!X#zO!Y#{O~O}!dO!S!Pa~OeaO!eiO!hjO!jkO#^$PO~P+qO!U#yO!X#zO!Y$SO~O}tO#^!|O#`$VO~O#^#PO#`$WO~P.uO#^$XO~OeaO!eiO!hjO!jkO#^#uO!U#_X!X#_X!Y#_X!^#_X~P+qOd$ZO~O!S$[O~O!Y$]O~O!X#zO!Y$]O~O!U#yO!X#zO!Y$_O~OeaO!eiO!hjO!jkO#^#uO!U#_P!X#_P!Y#_P~P+qO!Y$fO!^$eO~O!Y$hO~O!Y$iO~O!X#zO!Y$iO~O!S$kO~O!Y$mO~O!Y$nO~O!X#zO!Y$nO~O!U#yO!X#zO!Y$nO~O!Y$rO!^$eO~Or$tO!S$uO~O!Y$rO~O!Y$vO~O!Y$xO~O!X#zO!Y$xO~O!Y${O~O!Y%OO~Or$tO~O!S%PO~Ooz~",
goto: "4x#aPPPPPPPPPPPPPPPPPPPPPPPPPPP#b#w$aP%d#bP&k'bP(a(aPP(e)aP)u*g*jPP*pP*|+fPPP+|,zP-O-U-j.YP.bP.b.bP.bP.b.b.t.z/Q/W/^/h/o/y0T0Z0ePPP0l0p1^PP1v1|3fP4fPPPPPPPP4jPP4ppaOe|!]!^!n#c#j#k#l#v$O$Z$j$t%OR!W[t^O[e|!Z!]!^!n#c#j#k#l#v$O$Z$j$t%OT!jg$srWO[e|!]!^!n#c#j#k#l#v$O$Z$j$t%OzwRSWhjrsv{}!O!P!Q!R!S!g!y#P#^#_#pS!gg$sR#^!ZvSO[eg|!]!^!n#c#j#k#l#v$O$Z$j$s$t%OzTRSWhjrsv{}!O!P!Q!R!S!g!y#P#^#_#pQ!rkQ#]!YR#_!ZpYOe|!]!^!n#c#j#k#l#v$O$Z$j$t%OQ!U[S!ig$sQ!mhQ!pjQ#S!PR#U!O!rTORSW[eghjrsv{|}!O!P!Q!R!S!]!^!g!n!y#P#^#_#c#j#k#l#p#v$O$Z$j$s$t%OR#h!cTmPo!sTORSW[eghjrsv{|}!O!P!Q!R!S!]!^!g!n!y#P#^#_#c#j#k#l#p#v$O$Z$j$s$t%OQtR[ySW{!g#^#_Q!wrX!{t!w!|#npaOe|!]!^!n#c#j#k#l#v$O$Z$j$t%O[xSW{!g#^#_Q!W[R!zsR!ffX!df!b!e#gQ#|#dQ$T#mQ$`#}R$o$aQ#d!]Q#m!nQ$P#kQ$Q#lQ$k$ZQ$v$jQ$|$tR%P%OQ#{#dQ$S#mQ$]#|Q$_#}Q$i$TS$n$`$aR$x$o!QTRSW[ghjrsv{}!O!P!Q!R!S!g!y#P#^#_#p$sqUOe|!]!^!n#c#j#k#l#v$O$Z$j$t%OT$b$P$cQ$f$PR$r$cu^O[e|!Z!]!^!n#c#j#k#l#v$O$Z$j$t%OpZOe|!]!^!n#c#j#k#l#v$O$Z$j$t%OQ!V[Q!qjQ#W!RR#Z!S]ySW{!g#^#_qaOe|!]!^!n#c#j#k#l#v$O$Z$j$t%OQeOR!`eQoPR!toQrRR!vrQ!bfR#f!bQ!efQ#g!bT#i!e#gS#v#c$OR$X#vQ!|tQ#n!wT#r!|#nQ#PvQ#p!yT#s#P#pQ$c$PR$p$cY{SW!g#^#_R#Q{S![_!XR#a![TdOeSbOeQ#R|`#b!]!n#k#l$Z$j$t%OQ#e!^U#u#c#v$OR#}#jp_Oe|!]!^!n#c#j#k#l#v$O$Z$j$t%OQ!X[R#`!ZQ!kgR${$srXO[e|!]!^!n#c#j#k#l#v$O$Z$j$t%OQvR[xSW{!g#^#_S!hg$sQ!lhQ!ojQ!yrQ!zsW#Ov!y#P#pQ#S}Q#T!OQ#V!PQ#W!QQ#X!RR#Y!SpVOe|!]!^!n#c#j#k#l#v$O$Z$j$t%O!OwRSWghjrsv{}!O!P!Q!R!S!g!y#P#^#_#p$sR!T[TnPoQ#w#cR$a$O]zSW{!g#^#_", goto: "4z#cPPPPPPPPPPPPPPPPPPPPPPPPPPPP#d#y$cP%f#dP&m'dP(c(cPPP(g)cP)w*i*lPP*rP+O+hPPP,O,|P-Q-W-l.[P.dP.d.dP.dP.d.d.v.|/S/Y/`/j/q/{0V0]0gPPP0n0r1`PP1x2O3hP4hPPPPPPPP4lPP4rpbOf}!^!_!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", 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 DoubleQuote 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: 109, maxTerm: 111,
context: trackScope, context: trackScope,
nodeProps: [ nodeProps: [
["closedBy", 48,"end"] ["closedBy", 50,"end"]
], ],
propSources: [highlighting], propSources: [highlighting],
skippedNodes: [0,25], skippedNodes: [0,26],
repeatNodeCount: 11, 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!xYOt#{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#T~~(OO#R~U(VUtS!}QOt#{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#ZQtSOt#{uw#{x!_#{!_!`8S!`#O#{#P;'S#{;'S;=`$d<%lO#{U8XVtSOt#{uw#{x#O#{#P#Q8n#Q;'S#{;'S;=`$d<%lO#{U8uU#YQtSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~9^O#U~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[#VWtSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^@d[#XWtSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^Aa^#WWtSOt#{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#a~", tokenData: "FV~R}OX$OXY$mYZ%WZp$Opq$mqr$Ors%qst'wtu)}uw$Owx*Sxy*Xyz*rz{$O{|+]|}$O}!O.P!O!P$O!P!Q0f!Q![+z![!]9R!]!^%W!^!}$O!}#O9l#O#P;b#P#Q;g#Q#R$O#R#S<Q#S#T$O#T#Y/Q#Y#Z<k#Z#b/Q#b#cAi#c#f/Q#f#gBf#g#h/Q#h#iCc#i#o/Q#o#p$O#p#qEg#q;'S$O;'S;=`$g<%l~$O~O$O~~FQS$TUuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OS$jP;=`<%l$O^$tUuS!zYOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU%_UuS#^QOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU%vZuSOr%qrs&ist%qtu'Suw%qwx'Sx#O%q#O#P'S#P;'S%q;'S;=`'q<%lO%qU&pUxQuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OQ'VTOr'Srs'fs;'S'S;'S;=`'k<%lO'SQ'kOxQQ'nP;=`<%l'SU'tP;=`<%l%q^'|WuSOp$Opq(fqt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O^(mZjYuSOY(fYZ$OZt(ftu)`uw(fwx)`x#O(f#O#P)`#P;'S(f;'S;=`)w<%lO(fY)eSjYOY)`Z;'S)`;'S;=`)q<%lO)`Y)tP;=`<%l)`^)zP;=`<%l(f~*SO#V~~*XO#T~U*`UuS#PQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU*yUuS#aQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU+bWuSOt$Ouw$Ox!Q$O!Q![+z![#O$O#P;'S$O;'S;=`$g<%lO$OU,RYuSoQOt$Ouw$Ox!O$O!O!P,q!P!Q$O!Q![+z![#O$O#P;'S$O;'S;=`$g<%lO$OU,vWuSOt$Ouw$Ox!Q$O!Q![-`![#O$O#P;'S$O;'S;=`$g<%lO$OU-gWuSoQOt$Ouw$Ox!Q$O!Q![-`![#O$O#P;'S$O;'S;=`$g<%lO$OU.U^uSOt$Ouw$Ox}$O}!O/Q!O!Q$O!Q![+z![!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$OU/V[uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$OU0SU}QuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU0kWuSOt$Ouw$Ox!P$O!P!Q1T!Q#O$O#P;'S$O;'S;=`$g<%lO$OU1Y^uSOY2UYZ$OZt2Utu3Xuw2Uwx3Xx!P2U!P!Q$O!Q!}2U!}#O7z#O#P5g#P;'S2U;'S;=`8{<%lO2UU2]^uSzQOY2UYZ$OZt2Utu3Xuw2Uwx3Xx!P2U!P!Q5|!Q!}2U!}#O7z#O#P5g#P;'S2U;'S;=`8{<%lO2UQ3^XzQOY3XZ!P3X!P!Q3y!Q!}3X!}#O4h#O#P5g#P;'S3X;'S;=`5v<%lO3XQ3|P!P!Q4PQ4UUzQ#Z#[4P#]#^4P#a#b4P#g#h4P#i#j4P#m#n4PQ4kVOY4hZ#O4h#O#P5Q#P#Q3X#Q;'S4h;'S;=`5a<%lO4hQ5TSOY4hZ;'S4h;'S;=`5a<%lO4hQ5dP;=`<%l4hQ5jSOY3XZ;'S3X;'S;=`5v<%lO3XQ5yP;=`<%l3XU6RWuSOt$Ouw$Ox!P$O!P!Q6k!Q#O$O#P;'S$O;'S;=`$g<%lO$OU6rbuSzQOt$Ouw$Ox#O$O#P#Z$O#Z#[6k#[#]$O#]#^6k#^#a$O#a#b6k#b#g$O#g#h6k#h#i$O#i#j6k#j#m$O#m#n6k#n;'S$O;'S;=`$g<%lO$OU8P[uSOY7zYZ$OZt7ztu4huw7zwx4hx#O7z#O#P5Q#P#Q2U#Q;'S7z;'S;=`8u<%lO7zU8xP;=`<%l7zU9OP;=`<%l2UU9YUuS!SQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU9sW#]QuSOt$Ouw$Ox!_$O!_!`:]!`#O$O#P;'S$O;'S;=`$g<%lO$OU:bVuSOt$Ouw$Ox#O$O#P#Q:w#Q;'S$O;'S;=`$g<%lO$OU;OU#[QuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O~;gO#W~U;nU#`QuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU<XUuS!ZQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU<p]uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#U=i#U#o/Q#o;'S$O;'S;=`$g<%lO$OU=n^uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#`/Q#`#a>j#a#o/Q#o;'S$O;'S;=`$g<%lO$OU>o^uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#g/Q#g#h?k#h#o/Q#o;'S$O;'S;=`$g<%lO$OU?p^uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#X/Q#X#Y@l#Y#o/Q#o;'S$O;'S;=`$g<%lO$OU@s[yQuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$O^Ap[#XWuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$O^Bm[#ZWuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$O^Cj^#YWuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#f/Q#f#gDf#g#o/Q#o;'S$O;'S;=`$g<%lO$OUDk^uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#i/Q#i#j?k#j#o/Q#o;'S$O;'S;=`$g<%lO$OUEnU!cQuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O~FVO#c~",
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!|~~", 11)], tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#O~~", 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}], 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: 1634 tokenPrec: 1682
}) })

View File

@ -127,3 +127,52 @@ describe('string escape sequences', () => {
`) `)
}) })
}) })
describe('curly strings', () => {
test('work on one line', () => {
expect('{ one two three }').toMatchTree(`
String
CurlyString { one two three }
`)
})
test('work on multiple lines', () => {
expect(`{
one
two
three }`).toMatchTree(`
String
CurlyString {
one
two
three }`)
})
test('can contain other curlies', () => {
expect(`{ { one }
two
{ three } }`).toMatchTree(`
String
CurlyString { { one }
two
{ three } }`)
})
})
describe('double quoted strings', () => {
test("work", () => {
expect(`"hello world"`).toMatchTree(`
String
DoubleQuote "hello world"`)
})
test("don't interpolate", () => {
expect(`"hello $world"`).toMatchTree(`
String
DoubleQuote "hello $world"`)
expect(`"hello $(1 + 2)"`).toMatchTree(`
String
DoubleQuote "hello $(1 + 2)"`)
})
})

View File

@ -1,5 +1,5 @@
import { ExternalTokenizer, InputStream, Stack } from '@lezer/lr' import { ExternalTokenizer, InputStream, Stack } from '@lezer/lr'
import { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot, Do } 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) // doobie doobie do (we need the `do` keyword to know when we're defining params)
export function specializeKeyword(ident: string) { export function specializeKeyword(ident: string) {
@ -18,6 +18,10 @@ export const setGlobals = (newGlobals: string[]) => {
export const tokenizer = new ExternalTokenizer( export const tokenizer = new ExternalTokenizer(
(input: InputStream, stack: Stack) => { (input: InputStream, stack: Stack) => {
const ch = getFullCodePoint(input, 0) const ch = getFullCodePoint(input, 0)
// Handle curly strings
if (ch === 123 /* { */) return consumeCurlyString(input, stack)
if (!isWordChar(ch)) return if (!isWordChar(ch)) return
// Don't consume things that start with digits - let Number token handle it // Don't consume things that start with digits - let Number token handle it
@ -26,7 +30,7 @@ export const tokenizer = new ExternalTokenizer(
// Don't consume things that start with - or + followed by a digit (negative/positive numbers) // 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 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) const canBeWord = stack.canShift(Word)
// Consume all word characters, tracking if it remains a valid identifier // Consume all word characters, tracking if it remains a valid identifier
@ -119,13 +123,7 @@ const consumeWordToken = (
} }
// Track identifier validity: must be lowercase, digit, dash, or emoji/unicode // Track identifier validity: must be lowercase, digit, dash, or emoji/unicode
if ( if (!isIdentChar(ch)) {
!isLowercaseLetter(ch) &&
!isDigit(ch) &&
ch !== 45 /* - */ &&
ch !== 63 /* ? */ &&
!isEmojiOrUnicode(ch)
) {
if (!canBeWord) break if (!canBeWord) break
isValidIdentifier = false isValidIdentifier = false
} }
@ -157,6 +155,32 @@ const consumeRestOfWord = (input: InputStream, startPos: number, canBeWord: bool
return pos 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
let depth = 0
let pos = 0
while (true) {
const ch = input.peek(pos)
if (ch < 0) return // EOF - invalid
if (ch === 123) depth++ // {
else if (ch === 125) { // }
depth--
if (depth === 0) {
pos++ // consume final }
break
}
}
pos++
}
input.acceptToken(CurlyString, pos)
}
// Check if this identifier is in scope (for property access detection) // Check if this identifier is in scope (for property access detection)
// Returns IdentifierBeforeDot token if in scope, null otherwise // Returns IdentifierBeforeDot token if in scope, null otherwise
const checkForDotGet = (input: InputStream, stack: Stack, pos: number): number | null => { const checkForDotGet = (input: InputStream, stack: Stack, pos: number): number | null => {
@ -219,6 +243,14 @@ const chooseIdentifierToken = (input: InputStream, stack: Stack): number => {
} }
// Character classification helpers // 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 => { const isWhiteSpace = (ch: number): boolean => {
return ch === 32 /* space */ || ch === 9 /* tab */ || ch === 13 /* \r */ return ch === 32 /* space */ || ch === 9 /* tab */ || ch === 13 /* \r */
} }