Compare commits

...

15 Commits

17 changed files with 806 additions and 123 deletions

View File

@ -145,7 +145,7 @@ async function repl() {
} }
try { try {
const compiler = new Compiler(trimmed, Object.keys(globals)) const compiler = new Compiler(trimmed, [...Object.keys(globals), ...vm.vars()])
// Save VM state before appending bytecode, in case execution fails // Save VM state before appending bytecode, in case execution fails
const savedInstructions = [...vm.instructions] const savedInstructions = [...vm.instructions]
@ -235,7 +235,7 @@ async function loadFile(filePath: string): Promise<{ vm: VM; codeHistory: string
if (!trimmed) continue if (!trimmed) continue
try { try {
const compiler = new Compiler(trimmed) const compiler = new Compiler(trimmed, [...Object.keys(globals), ...vm.vars()])
vm.appendBytecode(compiler.bytecode) vm.appendBytecode(compiler.bytecode)
await vm.continue() await vm.continue()
codeHistory.push(trimmed) codeHistory.push(trimmed)

View File

@ -1,50 +1,14 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { Compiler } from '../src/compiler/compiler' import { colors } from '../src/prelude'
import { colors, globals } from '../src/prelude'
import { parser } from '../src/parser/shrimp'
import { treeToString } from '../src/utils/tree' import { treeToString } from '../src/utils/tree'
import { VM, fromValue, bytecodeToString } from 'reefvm' import { runFile, compileFile, parseCode } from '../src'
import { bytecodeToString } from 'reefvm'
import { readFileSync, writeFileSync, mkdirSync } from 'fs' import { readFileSync, writeFileSync, mkdirSync } from 'fs'
import { randomUUID } from 'crypto' import { randomUUID } from 'crypto'
import { spawn } from 'child_process' import { spawn } from 'child_process'
import { join } from 'path' import { join } from 'path'
async function runFile(filePath: string) {
try {
const code = readFileSync(filePath, 'utf-8')
const compiler = new Compiler(code, Object.keys(globals))
const vm = new VM(compiler.bytecode, globals)
await vm.run()
return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]) : null
} catch (error: any) {
console.error(`${colors.red}Error:${colors.reset} ${error.message}`)
process.exit(1)
}
}
async function compileFile(filePath: string) {
try {
const code = readFileSync(filePath, 'utf-8')
const compiler = new Compiler(code)
return bytecodeToString(compiler.bytecode)
} catch (error: any) {
console.error(`${colors.red}Error:${colors.reset} ${error.message}`)
process.exit(1)
}
}
async function parseFile(filePath: string) {
try {
const code = readFileSync(filePath, 'utf-8')
const tree = parser.parse(code)
return treeToString(tree, code)
} catch (error: any) {
console.error(`${colors.red}Error:${colors.reset} ${error.message}`)
process.exit(1)
}
}
function showHelp() { function showHelp() {
console.log(`${colors.bright}${colors.magenta}🦐 Shrimp${colors.reset} is a scripting language in a shell. console.log(`${colors.bright}${colors.magenta}🦐 Shrimp${colors.reset} is a scripting language in a shell.
@ -112,7 +76,7 @@ async function main() {
console.log(`${colors.bright}usage: shrimp bytecode <file>${colors.reset}`) console.log(`${colors.bright}usage: shrimp bytecode <file>${colors.reset}`)
process.exit(1) process.exit(1)
} }
console.log(await compileFile(file)) console.log(bytecodeToString(compileFile(file)))
return return
} }
@ -122,7 +86,8 @@ async function main() {
console.log(`${colors.bright}usage: shrimp parse <file>${colors.reset}`) console.log(`${colors.bright}usage: shrimp parse <file>${colors.reset}`)
process.exit(1) process.exit(1)
} }
console.log(await parseFile(file)) const input = readFileSync(file, 'utf-8')
console.log(treeToString(parseCode(input), input))
return return
} }

View File

@ -44,7 +44,7 @@
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], "@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
@ -52,7 +52,7 @@
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.15", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-qtAXMNGG4R0UGGI8zWrqm2B7BdXqx48vunJXBPzfDOHPA5WkRUZdTSbE7TFwO4jLhYqSE23YMWsM9NhE6ovobw=="], "bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.15", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-qtAXMNGG4R0UGGI8zWrqm2B7BdXqx48vunJXBPzfDOHPA5WkRUZdTSbE7TFwO4jLhYqSE23YMWsM9NhE6ovobw=="],
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="], "codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
@ -62,11 +62,11 @@
"hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="], "hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="],
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#bffb83a5280a4d74e424c4e0f4fbd46f790227a3", { "peerDependencies": { "typescript": "^5" } }, "bffb83a5280a4d74e424c4e0f4fbd46f790227a3"], "reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#d7a971db24aea5ddcaae2c18ce9f10dab793db19", { "peerDependencies": { "typescript": "^5" } }, "d7a971db24aea5ddcaae2c18ce9f10dab793db19"],
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
"tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="], "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],

View File

@ -58,9 +58,9 @@ export class Compiler {
bytecode: Bytecode bytecode: Bytecode
pipeCounter = 0 pipeCounter = 0
constructor(public input: string, globals?: string[]) { constructor(public input: string, globals?: string[] | Record<string, any>) {
try { try {
if (globals) setGlobals(globals) if (globals) setGlobals(Array.isArray(globals) ? globals : Object.keys(globals))
const cst = parser.parse(input) const cst = parser.parse(input)
const errors = checkTreeForErrors(cst) const errors = checkTreeForErrors(cst)

View File

@ -1,11 +1,17 @@
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import { VM, fromValue, type Bytecode } from 'reefvm' import { VM, fromValue, toValue, isValue, type Bytecode } from 'reefvm'
import { type Tree } from '@lezer/common'
import { Compiler } from '#compiler/compiler' import { Compiler } from '#compiler/compiler'
import { globals as shrimpGlobals, colors } from '#prelude' import { parser } from '#parser/shrimp'
import { globals as parserGlobals, setGlobals as setParserGlobals } from '#parser/tokenizer'
import { globals as shrimpGlobals } from '#prelude'
export { Compiler } from '#compiler/compiler' export { Compiler } from '#compiler/compiler'
export { parser } from '#parser/shrimp' export { parser } from '#parser/shrimp'
export { globals } from '#prelude' export { globals as prelude } from '#prelude'
export type { Tree } from '@lezer/common'
export { type Value, type Bytecode } from 'reefvm'
export { toValue, fromValue, isValue, Scope, VM, bytecodeToString } from 'reefvm'
export class Shrimp { export class Shrimp {
vm: VM vm: VM
@ -17,6 +23,32 @@ export class Shrimp {
this.globals = globals this.globals = globals
} }
get(name: string): any {
const value = this.vm.scope.get(name)
return value ? fromValue(value, this.vm) : null
}
set(name: string, value: any) {
this.vm.scope.set(name, toValue(value, this.vm))
}
has(name: string): boolean {
return this.vm.scope.has(name)
}
async call(name: string, ...args: any[]): Promise<any> {
const result = await this.vm.call(name, ...args)
return isValue(result) ? fromValue(result, this.vm) : result
}
parse(code: string): Tree {
return parseCode(code, this.globals)
}
compile(code: string): Bytecode {
return compileCode(code, this.globals)
}
async run(code: string | Bytecode, locals?: Record<string, any>): Promise<any> { async run(code: string | Bytecode, locals?: Record<string, any>): Promise<any> {
let bytecode let bytecode
@ -32,13 +64,9 @@ export class Shrimp {
await this.vm.continue() await this.vm.continue()
if (locals) this.vm.popScope() if (locals) this.vm.popScope()
return this.vm.stack.length ? fromValue(this.vm.stack.at(-1)!) : null return this.vm.stack.length ? fromValue(this.vm.stack.at(-1)!, this.vm) : null
} }
get(name: string): any {
const value = this.vm.scope.get(name)
return value ? fromValue(value) : null
}
} }
export async function runFile(path: string, globals?: Record<string, any>): Promise<any> { export async function runFile(path: string, globals?: Record<string, any>): Promise<any> {
@ -51,14 +79,9 @@ export async function runCode(code: string, globals?: Record<string, any>): Prom
} }
export async function runBytecode(bytecode: Bytecode, globals?: Record<string, any>): Promise<any> { export async function runBytecode(bytecode: Bytecode, globals?: Record<string, any>): Promise<any> {
try {
const vm = new VM(bytecode, Object.assign({}, shrimpGlobals, globals)) const vm = new VM(bytecode, Object.assign({}, shrimpGlobals, globals))
await vm.run() await vm.run()
return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]!) : null return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]!, vm) : null
} catch (error: any) {
console.error(`${colors.red}Error:${colors.reset} ${error.message}`)
process.exit(1)
}
} }
export function compileFile(path: string, globals?: Record<string, any>): Bytecode { export function compileFile(path: string, globals?: Record<string, any>): Bytecode {
@ -71,3 +94,19 @@ export function compileCode(code: string, globals?: Record<string, any>): Byteco
const compiler = new Compiler(code, globalNames) const compiler = new Compiler(code, globalNames)
return compiler.bytecode return compiler.bytecode
} }
export function parseFile(path: string, globals?: Record<string, any>): Tree {
const code = readFileSync(path, 'utf-8')
return parseCode(code, globals)
}
export function parseCode(code: string, globals?: Record<string, any>): Tree {
const oldGlobals = [...parserGlobals]
const globalNames = [...Object.keys(shrimpGlobals), ...(globals ? Object.keys(globals) : [])]
setParserGlobals(globalNames)
const result = parser.parse(code)
setParserGlobals(oldGlobals)
return result
}

View File

@ -12,13 +12,13 @@
@precedence { Number Regex } @precedence { Number Regex }
StringFragment { !['\\$]+ } StringFragment { !['\\$]+ }
NamedArgPrefix { $[a-z-]+ "=" } NamedArgPrefix { $[a-z] $[a-z0-9-]* "=" }
Number { ("-" | "+")? $[0-9]+ ('.' $[0-9]+)? } Number { ("-" | "+")? $[0-9]+ ("_"? $[0-9]+)* ('.' $[0-9]+ ("_"? $[0-9]+)*)? }
Boolean { "true" | "false" } Boolean { "true" | "false" }
newlineOrSemicolon { "\n" | ";" } newlineOrSemicolon { "\n" | ";" }
eof { @eof } eof { @eof }
space { " " | "\t" } space { " " | "\t" }
Comment { "#" " " ![\n]* } Comment { "#" ![\n]* }
leftParen { "(" } leftParen { "(" }
rightParen { ")" } rightParen { ")" }
colon[closedBy="end", @name="colon"] { ":" } colon[closedBy="end", @name="colon"] { ":" }
@ -47,7 +47,8 @@ null { @specialize[@name=Null]<Identifier, "null"> }
comparison @left, comparison @left,
multiplicative @left, multiplicative @left,
additive @left, additive @left,
call call,
functionWithNewlines
} }
item { item {
@ -188,7 +189,21 @@ BinOp {
} }
ParenExpr { ParenExpr {
leftParen (IfExpr | ambiguousFunctionCall | BinOp | expressionWithoutIdentifier | ConditionalOp | PipeExpr | FunctionDef) rightParen leftParen newlineOrSemicolon* (
FunctionCallWithNewlines |
IfExpr |
ambiguousFunctionCall |
BinOp newlineOrSemicolon* |
expressionWithoutIdentifier |
ConditionalOp newlineOrSemicolon* |
PipeExpr |
FunctionDef
)
rightParen
}
FunctionCallWithNewlines[@name=FunctionCall] {
(DotGet | Identifier | ParenExpr) newlineOrSemicolon+ arg !functionWithNewlines (newlineOrSemicolon+ arg)* newlineOrSemicolon*
} }
expression { expression {

View File

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

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,while:60, if:68, null:96, catch:102, finally:108, end:110, else:118, try:136, throw:140} const spec_Identifier = {__proto__:null,while:60, null:92, catch:98, finally:104, end:106, if:114, else:120, try:138, throw:142}
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, version: 14,
states: "9UQYQbOOO!dOpO'#DQO!iOSO'#DXO$_QcO'#DkO&rQcO'#EYOOQ`'#Eh'#EhO'uQRO'#DlO)[QcO'#EWO)lQbO'#C|OOQa'#Dn'#DnO+nQbO'#DoOOQa'#EY'#EYO+uQcO'#EYO+|QcO'#EXO,yQcO'#EWO-TQRO'#DuOOQ`'#EW'#EWO-iQbO'#EWO-pQQO'#EVOOQ`'#EV'#EVOOQ`'#Dw'#DwQYQbOOO-{QbO'#DTO.WQbO'#C}O.{QbO'#CyO/pQQO'#DqO.{QbO'#DsO/uObO,59lO0QQbO'#DZO0YQWO'#D[OOOO'#E`'#E`OOOO'#D|'#D|O0nOSO,59sOOQa,59s,59sOOQ`'#DS'#DSO0|QbO'#DgOOQ`'#E^'#E^OOQ`'#Dy'#DyO1WQbO,59kOOQa'#EX'#EXO.{QbO,5:WO.{QbO,5:WO.{QbO,5:WO.{QbO,59gO.{QbO,59gO.{QbO,59gO2QQRO,59hO2^QQO,59hO2fQQO,59hO2qQRO,59hO3[QRO,59hO3jQQO'#CwOOQ`'#EP'#EPO3oQbO,5:ZO3vQQO,5:YOOQa,5:Z,5:ZO4RQbO,5:ZO)lQbO,5:bO)lQbO,5:aO4]QbO,5:[O4dQbO,59cOOQ`,5:q,5:qO)lQbO'#DxOOQ`-E7u-E7uOOQ`'#Dz'#DzO5OQbO'#DUO5ZQbO'#DVOOQO'#D{'#D{O5RQQO'#DUO5iQQO,59oO5nQcO'#EXO5uQRO'#E[O6lQRO'#E[OOQO'#E['#E[O6sQQO,59iO6xQRO,59eO7PQRO,59eO4]QbO,5:]O7[QcO,5:_O8dQcO,5:_O8tQcO,5:_OOQa1G/W1G/WOOOO,59u,59uOOOO,59v,59vOOOO-E7z-E7zOOQa1G/_1G/_OOQ`,5:R,5:ROOQ`-E7w-E7wOOQa1G/r1G/rO9sQcO1G/rO9}QcO1G/rO:XQcO1G/rOOQa1G/R1G/RO<TQcO1G/RO<[QcO1G/RO<cQcO1G/ROOQa1G/S1G/SOOQ`-E7}-E7}O<jQQO1G/tOOQa1G/u1G/uO<uQbO1G/uOOQO'#EQ'#EQO<jQQO1G/tOOQa1G/t1G/tOOQ`'#ER'#ERO<uQbO1G/uO=PQbO1G/|O=kQbO1G/{O>VQbO'#DbO>hQbO'#DbO>{QbO1G/vOOQ`-E7v-E7vOOQ`,5:d,5:dOOQ`-E7x-E7xO?WQQO,59pOOQO,59q,59qOOQO-E7y-E7yO?`QbO1G/ZO4]QbO1G/TO4]QbO1G/PO?gQbO1G/wO?rQQO7+%`OOQa7+%`7+%`O?}QbO7+%aOOQa7+%a7+%aOOQO-E8O-E8OOOQ`-E8P-E8POOQ`'#D}'#D}O@XQQO'#D}O@aQbO'#EgOOQ`,59|,59|O@tQbO'#D`O@yQQO'#DcOOQ`7+%b7+%bOAOQbO7+%bOATQbO7+%bOA]QbO7+$uOAkQbO7+$uOA{QbO7+$oOBTQbO7+$kOOQ`7+%c7+%cOBYQbO7+%cOB_QbO7+%cOOQa<<Hz<<HzOOQa<<H{<<H{OOQ`,5:i,5:iOOQ`-E7{-E7{OBgQQO,59zO4]QbO,59}OOQ`<<H|<<H|OBlQbO<<H|OOQ`<<Ha<<HaOBqQbO<<HaOBvQbO<<HaOCOQbO<<HaOOQ`'#EO'#EOOCZQbO<<HZOCcQbO'#DjOOQ`<<HZ<<HZOCkQbO<<HZOOQ`<<HV<<HVOOQ`<<H}<<H}OCpQbO<<H}O4]QbO1G/fOOQ`1G/i1G/iOOQ`AN>hAN>hOOQ`AN={AN={OCuQbOAN={OCzQbOAN={OOQ`-E7|-E7|OOQ`AN=uAN=uODSQbOAN=uO.WQbO,5:SO4]QbO,5:UOOQ`AN>iAN>iOOQ`7+%Q7+%QOOQ`G23gG23gODXQbOG23gPD^QbO'#DhOOQ`G23aG23aODcQQO1G/nOOQ`1G/p1G/pOOQ`LD)RLD)RO4]QbO7+%YOOQ`<<Ht<<Ht", states: "<OQYQbOOO!dOpO'#DOO!iOSO'#DVO$wQcO'#DlO'[QcO'#E[OOQ`'#Ej'#EjO'uQRO'#DmO)[QcO'#EYO)lQbO'#C|OOQa'#Do'#DoO+qQbO'#DpOOQa'#E['#E[O+xQcO'#E[O,cQcO'#EZO-`QcO'#EYO-jQRO'#DvOOQ`'#EY'#EYO.OQbO'#EYO.VQQO'#EXOOQ`'#EX'#EXOOQ`'#Dx'#DxQYQbOOO.bQbO'#DRO.mQbO'#DfO/bQbO'#CyO0VQQO'#DrO/bQbO'#DtO0[ObO,59jO0gQbO'#DXO0oQWO'#DYOOOO'#Eb'#EbOOOO'#D}'#D}O1TOSO,59qOOQa,59q,59qOOQ`'#DQ'#DQO1cQbO'#DeOOQ`'#E`'#E`OOQ`'#EQ'#EQO1mQbO,5:SOOQa'#EZ'#EZO/bQbO,5:XO/bQbO,5:XO/bQbO,5:XO/bQbO,59gO/bQbO,59gO/bQbO,59gOOQ`'#Dz'#DzO)lQbO,59hO2gQcO'#DlO2nQcO'#E[O3ZQRO,59hO3bQQO,59hO3gQQO,59hO3oQQO,59hO4xQRO,59hO5PQRO,59hO5bQQO'#CwO5gQbO,5:[O5nQQO,5:ZOOQa,5:[,5:[O5yQbO,5:[O6TQbO,5:cO6TQbO,5:bO7[QbO,5:]O7cQbO,59cOOQ`,5:s,5:sO6TQbO'#DyOOQ`-E7v-E7vOOQ`'#D{'#D{O7}QbO'#DSO8YQbO'#DTOOQO'#D|'#D|O8QQQO'#DSO8hQQO,59mO8mQcO'#EZO9gQRO'#EiO:^QRO'#EiOOQO'#Ei'#EiO:eQQO,5:QO:jQRO,59eO:qQRO,59eO7[QbO,5:^O:|QcO,5:`O<UQcO,5:`O<fQcO,5:`OOQa1G/U1G/UOOOO,59s,59sOOOO,59t,59tOOOO-E7{-E7{OOQa1G/]1G/]OOQ`,5:P,5:POOQ`-E8O-E8OOOQa1G/s1G/sO=eQcO1G/sO=oQcO1G/sO=yQcO1G/sOOQa1G/R1G/RO?uQcO1G/RO?|QcO1G/RO@TQcO1G/ROOQ`-E7x-E7xO@[QRO1G/SO@cQQO1G/SO@hQQO1G/SO@pQQO1G/SO@{QRO1G/SOASQRO1G/SOAeQbO,59iOAoQQO1G/SOOQa1G/S1G/SOAwQQO1G/uOOQa1G/v1G/vOBSQbO1G/vOOQO'#ES'#ESOAwQQO1G/uOOQa1G/u1G/uOOQ`'#ET'#ETOBSQbO1G/vOB^QbO1G/}OBxQbO1G/|OCdQbO'#D`OCuQbO'#D`ODYQbO1G/wOOQ`-E7w-E7wOOQ`,5:e,5:eOOQ`-E7y-E7yODeQQO,59nOOQO,59o,59oOOQO-E7z-E7zODmQbO1G/XO7[QbO1G/lO7[QbO1G/PODtQbO1G/xOEPQQO7+$nOOQa7+$n7+$nOEXQQO1G/TOEaQQO7+%aOOQa7+%a7+%aOElQbO7+%bOOQa7+%b7+%bOOQO-E8Q-E8QOOQ`-E8R-E8ROOQ`'#EO'#EOOEvQQO'#EOOFOQbO'#EhOOQ`,59z,59zOFcQbO'#D^OFhQQO'#DaOOQ`7+%c7+%cOFmQbO7+%cOFrQbO7+%cOFzQbO7+$sOGYQbO7+$sOGjQbO7+%WOGrQbO7+$kOOQ`7+%d7+%dOGwQbO7+%dOG|QbO7+%dOOQa<<HY<<HYOHUQbO7+$oOHcQQO7+$oOOQa<<H{<<H{OOQa<<H|<<H|OOQ`,5:j,5:jOOQ`-E7|-E7|OHkQQO,59xO7[QbO,59{OOQ`<<H}<<H}OHpQbO<<H}OOQ`<<H_<<H_OHuQbO<<H_OHzQbO<<H_OISQbO<<H_OOQ`'#ER'#EROI_QbO<<HrOIgQbO'#DkOOQ`<<Hr<<HrOIoQbO<<HrOOQ`<<HV<<HVOOQ`<<IO<<IOOItQbO<<IOOOQO,5:k,5:kOIyQbO<<HZOOQO-E7}-E7}O7[QbO1G/dOOQ`1G/g1G/gOOQ`AN>iAN>iOOQ`AN=yAN=yOJWQbOAN=yOJ]QbOAN=yOOQ`-E8P-E8POOQ`AN>^AN>^OJeQbOAN>^O.mQbO,5:TO7[QbO,5:VOOQ`AN>jAN>jPAeQbO'#DzOOQ`7+%O7+%OOOQ`G23eG23eOJjQbOG23ePIjQbO'#DiOOQ`G23xG23xOJoQQO1G/oOOQ`1G/q1G/qOOQ`LD)PLD)PO7[QbO7+%ZOOQ`<<Hu<<Hu",
stateData: "Dk~O!xOSiOS~OdROe_OfZOgPOhfOnhOrgOuZO!PZO!QZO!aZO!fiO!hjO!}WO#RQO#YcO#^XO#_YO~O#PkO~O|nO#RqO#TlO#UmO~OdwOfZOgPOhfOuZOzsO!PZO!QZO!YrO!aZO!}WO#RQO#^XO#_YOT!{XU!{XW!{XX!{XY!{XZ!{X[!{X]!{X~OP!{XQ!{XR!{XS!{X^!{Xl!_X!R!_X#Y!_X#a!_X#]!_X!T!_X!W!_X!X!_X!]!_X~P!wOP!|XQ!|XR!|XS!|XT!|XU!|XW!|XX!|XY!|XZ!|X[!|X]!|X^!|Xl!|X#Y!|X#a!|X#]!|X!T!|X!W!|X!X!|X!]!|X~OdwOfZOgPOhfOuZOzsO!PZO!QZO!YrO!aZO!}WO#RQO#^XO#_YO!R!|X~P%_OPyOQyORzOSzOT|OU}OW{OX{OY{OZ{O[{O]{O^xO~Ol!zX#Y!zX#a!zX!T!zX!W!zX!X!zX#]!zX!]!zX~OPyOQyORzOSzO~P(pOdROe_OfZOgPOhfOnhOrgOuZO!PZO!QZO!aZO!fiO!hjO!}WO#RQO#^XO#_YO~OdwOfZOgPOuZOzsO!PZO!QZO!aZO!}WO#RQO#Y!UO#^XO#_YO~O#`!XO~P*sOV!ZO~P%_OP!{XQ!{XR!{XS!{XT!{XU!{XW!{XX!{XY!{XZ!{X[!{X]!{X^!{X~P(pOT|OU}O~P(pOV!ZO_![O`![Oa![Ob![Oc![O~O!R!]O~P(pOl!`O#Y!_O#a!_O~Od!bOz!dO!RxP~Od!hOfZOgPOuZO!PZO!QZO!aZO!}WO#RQO#^XO#_YO~OdwOfZOgPOuZO!PZO!QZO!aZO!}WO#RQO#^XO#_YO~O!R!oO~Od!sOu!sO!}WO~Od!tO!}WO~O#R!uO#T!uO#U!uO#V!uO#W!uO#X!uO~O|nO#R!wO#TlO#UmO~OhfO!Y!xO~P.{OhfOzsO!YrOlsa!Rsa#Ysa#asa#]sa!Tsa!Wsa!Xsa!]sa~P.{OPyOQyORzOSzO#]#SOl!zX~O!R!]O#]#SOl!zX~O#]#SOP!{XQ!{XR!{XS!{X^!{Xl!zX~P#sOT|OU}O#]#SOl!zX~Ol!`O~O#`#VO~P*sOzsO#Y#XO#`#ZO~O#Y#[O#`#VO~P.{O#Y#aO~P)lOl!`O#Yka#aka#]ka!Tka!Wka!Xka!]ka~Od!bOz!dO!RxX~Ou#gO!P#gO!Q#gO#RQO~O!R#iO~O!R!{X~P!wOT|OU}O!R#OX~OT|OU}OW{OX{OY{OZ{O[{O]{O~O!R#OX~P6QO!R#jO~O!R#kO~P6QOT|OU}O!R#kO~Ol!ga#Y!ga#a!ga!T!ga!W!ga!X!ga#]!ga!]!ga~P'uOl!ga#Y!ga#a!ga!T!ga!W!ga!X!ga#]!ga!]!ga~OPyOQyORzOSzO~P7xOT|OU}O~P7xO^xOR!`iS!`il!`i#Y!`i#a!`i#]!`i!T!`i!W!`i!X!`i!]!`i~OP!`iQ!`i~P9OOPyOQyO~P9OOPyOQyOR!`iS!`il!`i#Y!`i#a!`i#]!`i!T!`i!W!`i!X!`i!]!`i~OW{OX{OY{OZ{O[{O]{OToiloi#Yoi#aoi#]oi!Roi!Toi!Woi!Xoi!]oi~OU}O~P;POU}O~P;cOUoi~P;POzsO#Y#XO#`#nO~O#Y#[O#`#pO~P.{Ol!`O#Y!ji#a!ji!T!ji!W!ji!X!ji#]!ji!]!ji~Ol!`O#Y!ii#a!ii!T!ii!W!ii!X!ii#]!ii!]!ii~Ol!`O!T!UX!W!UX!X!UX!]!UX~O#Y#sO!T#ZP!W#ZP!X#ZP!]#ZP~P)lO!T#wO!W#xO!X#yO~Oz!dO!Rxa~O#Y#}O~P)lO!T#wO!W#xO!X$QO~OzsO#Y#XO#`$TO~O#Y#[O#`$UO~P.{Ol!`O#Y$VO~O#Y#sO!T#ZX!W#ZX!X#ZX!]#ZX~P)lOd$XO~O!R$YO~O!X$ZO~O!W#xO!X$ZO~Ol!`O!T#wO!W#xO!X$]O~O#Y#sO!T#ZP!W#ZP!X#ZP~P)lO!X$dO!]$cO~O!X$fO~O!X$gO~O!W#xO!X$gO~O!R$iO~O!X$kO~O!X$lO~O!W#xO!X$lO~O!T#wO!W#xO!X$lO~O!X$pO!]$cO~Or$rO!R$sO~O!X$pO~O!X$tO~O!X$vO~O!W#xO!X$vO~O!X$yO~O!X$|O~Or$rO~O!R$}O~Ou!a~", stateData: "Jw~O!zOSiOS~OdROe_OfZOgPOhfOnhOsZO}ZO!OZO!ZgO!bZO!giO!ijO#PWO#QcO#TQO#`XO#aYO~O#RkO~OznO#TqO#VlO#WmO~OdwOfZOgPOhfOsZOxsO}ZO!OZO!WrO!bZO#PWO#TQO#`XO#aYOP!}XQ!}XR!}XS!}XT!}XU!}XW!}XX!}XY!}XZ!}X[!}X]!}X^!}Xl!`X!P!`X#_!`X~O#Q!`X#c!`X!R!`X!U!`X!V!`X!^!`X~P!wOdwOfZOgPOhfOsZOxsO}ZO!OZO!WrO!bZO#PWO#TQO#`XO#aYOP#OXQ#OXR#OXS#OXT#OXU#OXW#OXX#OXY#OXZ#OX[#OX]#OX^#OXl#OX#_#OX~O#Q#OX#c#OX!P#OX!R#OX!U#OX!V#OX!^#OX~P%_OPyOQyORzOSzOT|OU}OW{OX{OY{OZ{O[{O]{O^xO~Ol!|X#Q!|X#c!|X!R!|X!U!|X!V!|X#_!|X!^!|X~OPyOQyORzOSzO~P(pOd!QOe_OfZOgPOhfOnhOsZO}ZO!OZO!ZgO!bZO!giO!ijO#PWO#Q!OO#TQO#`XO#aYO~OdwOfZOgPOsZOxsO}ZO!OZO!bZO#PWO#Q!OO#TQO#`XO#aYO~O#b!]O~P*vOV!_O#Q#OX#c#OX!R#OX!U#OX!V#OX!^#OX~P&ZOP!}XQ!}XR!}XS!}XT!}XU!}XW!}XX!}XY!}XZ!}X[!}X]!}X^!}X~P(pOT|OU}O~P(pOV!_O_!`O`!`Oa!`Ob!`Oc!`O~O!P!aO~P(pOl!dO#Q!cO#c!cO~Od!fOx!hO!PvP~Od!lOfZOgPOsZO}ZO!OZO!bZO#PWO#TQO#`XO#aYO~OdwOfZOgPOsZO}ZO!OZO!bZO#PWO#TQO#`XO#aYO~O!P!sO~Od!wOs!wO#PWO~Od!xO#PWO~O#T!yO#V!yO#W!yO#X!yO#Y!yO#Z!yO~OznO#T!{O#VlO#WmO~OhfO!W!|O~P/bOhfOxsO!WrOl![a!P![a#Q![a#c![a#_![a!R![a!U![a!V![a!^![a~P/bO#Q!OO~P!wO#Q!OO~P%_OPyOQyORzOSzO#Q!OOl!|X~O#_#aO~P2uO#_#aO~O#_#aOl!|X~O!P!aO#_#aOl!|X~OP!}XQ!}XR!}XS!}XT!}XU!}XW!}XX!}XY!}XZ!}X[!}X]!}X^!}Xl!|X~O#_#aO~P3zOT|OU}O#Q!OO#_#aOl!|X~Ol!dO~O#b#cO~P*vOxsO#Q#eO#b#gO~O#Q#hO#b#cO~P/bOdROe_OfZOgPOhfOnhOsZO}ZO!OZO!ZgO!bZO!giO!ijO#PWO#TQO#`XO#aYO~O#Q#mO~P6TOl!dO#Qka#cka#_ka!Rka!Uka!Vka!^ka~Od!fOx!hO!PvX~Os#sO}#sO!O#sO#TQO~O!P#uO~OhfOxsO!WrOT!}XU!}XW!}XX!}XY!}XZ!}X[!}X]!}X!P!}X~P/bOT|OU}O!P#]X~OT|OU}OW{OX{OY{OZ{O[{O]{O~O!P#]X~P9rO!P#vO~O!P#wO~P9rOT|OU}O!P#wO~Ol!ha#Q!ha#c!ha!R!ha!U!ha!V!ha#_!ha!^!ha~P'uOl!ha#Q!ha#c!ha!R!ha!U!ha!V!ha#_!ha!^!ha~OPyOQyORzOSzO~P;jOT|OU}O~P;jO^xOR!aiS!ail!ai#Q!ai#c!ai#_!ai!R!ai!U!ai!V!ai!^!ai~OP!aiQ!ai~P<pOPyOQyO~P<pOPyOQyOR!aiS!ail!ai#Q!ai#c!ai#_!ai!R!ai!U!ai!V!ai!^!ai~OW{OX{OY{OZ{O[{O]{OToiloi#Qoi#coi#_oi!Poi!Roi!Uoi!Voi!^oi~OU}O~P>qOU}O~P?TOUoi~P>qO#_#zO~P2uO#_#zO~O#_#zOl!|X~O!P!aO#_#zOl!|X~O#_#zO~P3zOT|OU}O#Q!OO#_#zOl!|X~OhfO!WrO~P*vO#Q!OO#_#zO~OxsO#Q#eO#b#}O~O#Q#hO#b$PO~P/bOl!dO#Q!ki#c!ki!R!ki!U!ki!V!ki#_!ki!^!ki~Ol!dO#Q!ji#c!ji!R!ji!U!ji!V!ji#_!ji!^!ji~Ol!dO!R!SX!U!SX!V!SX!^!SX~O#Q$SO!R#[P!U#[P!V#[P!^#[P~P6TO!R$WO!U$XO!V$YO~Ox!hO!Pva~O#Q$^O~P6TO!R$WO!U$XO!V$aO~O#Q!OO#_$dO~O#Q!OO#_qi~OxsO#Q#eO#b$gO~O#Q#hO#b$hO~P/bOl!dO#Q$iO~O#Q$SO!R#[X!U#[X!V#[X!^#[X~P6TOd$kO~O!P$lO~O!V$mO~O!U$XO!V$mO~Ol!dO!R$WO!U$XO!V$oO~O#Q$SO!R#[P!U#[P!V#[P~P6TO!V$vO!^$uO~O!V$xO~O!V$yO~O!U$XO!V$yO~OhfO!WrO#_qq~P*vO#Q!OO#_qq~O!P%OO~O!V%QO~O!V%RO~O!U$XO!V%RO~O!R$WO!U$XO!V%RO~O!V%VO!^$uO~O!P%YO!Z%XO~O!V%VO~O!V%ZO~OhfO!WrO#_qy~P*vO!V%^O~O!U$XO!V%^O~O!V%aO~O!V%dO~O!P%eO~Os!b~",
goto: "4l#]PPPPPPPPPPPPPPPPPPPPPPPPPPP#^P#tP$Y%Q#^P&T&mP'l'r(c(fP(lP)j)jPPP)nP)z*dPPP*z+^P+b+h+|P,m-h#t#tP#tP#t#t.e.k.w/P/V/a/g/n/t/z0UPPP0`0d1W2oP3nP3tP3zPPPPPP4O4Ur`Oe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}R!PWu`OWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}r^Oe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}Q!SWS!ig$rQ!nhQ!rjQ#O}R#Q|xSOWeg!Z![!]!`!o#a#i#j#k#u#}$Y$i$r$s$}vZRSYhjsvxyz{|}!V!Y!h#W#]#oQ!skR!tltTOWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}T!kg$rtROWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}vwRSYhjsvxyz{|}!V!Y!h#W#]#oT!hg$rXtRSv!hr`Oe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}WrRSv!hQ!PWR!xsR!gfX!ef!c!f#f!pZORSWYeghjsvxyz{|}!V!Y!Z![!]!`!h!o#W#]#a#i#j#k#o#u#}$Y$i$r$s$}R#g!dTnQpQ#{#bQ$S#lQ$_#|R$n$`Q#b!]Q#l!oQ$O#jQ$P#kQ$j$YQ$u$iQ${$sR%O$}Q#z#bQ$R#lQ$[#{Q$^#|Q$h$SS$m$_$`R$w$nWtRSv!hQ!WYQ#U!VX#X!W#U#Y#mT$a$O$bQ$e$OR$q$buTOWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}rVOe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}Q!OWQ!qjQ!zyR!}z!qZORSWYeghjsvxyz{|}!V!Y!Z![!]!`!h!o#W#]#a#i#j#k#o#u#}$Y$i$r$s$}zZRSYghjsvxyz{|}!V!Y!h#W#]#o$ru[OWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}QeOR!ae^!^b!T#^#_#`#t#|R#c!^UvRS!hR!yvQ!cfR#e!cQ!ffQ#f!cT#h!f#fQpQR!vpS#u#a#}R$W#uQ$b$OR$o$bQ!VYR#T!VQ#Y!WQ#m#UT#q#Y#mQ#]!YQ#o#WT#r#]#oTdOeSbOeQ!TWQ#^!ZQ#_![`#`!]!o#j#k$Y$i$s$}Q#d!`U#t#a#u#}R#|#itUOWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}WrRSv!hQ!YYS!jg$rQ!mhQ!pjQ!xsQ!zxQ!{yQ!|zQ#O{Q#P|Q#R}Q#W!VX#[!Y#W#]#or]Oe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}zwRSYghjsvxyz{|}!V!Y!h#W#]#o$rR!RWQ!lgR$z$rXuRSv!hToQpQ#v#aR$`#}raOe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}R!QW", goto: "7t#_PPPPPPPPPPPPPPPPPPPPPPPPPPP#`P#yP$`%Z&g&mP'u(R({)OP)UP*Z*ZPPP*_P*k+TPPP+k#`P,T,nP,r,x-_P.R/T#y#yP#yP#y#y0X0_0k1_1e1o1u1|2S2^2d2nPPP2x2|3q5aPPP6iP6yPPPPP6}7T7Zr`Oe!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%eQ!UWR#Z!Pw`OWe!P!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%er^Oe!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%eQ!XWS!mg%XQ!rhQ!vjQ#S}Q#U|R#^!PvSOeg!_!`!a!d!s#m#u#v#w$U$^$l%O%X%Y%e!SZRSYhjsvxyz{|}!Q!R!Z!^!l#_#d#i$O$e$|%[S!RW!PQ!wkR!xlQ!TWR#Y!PrROe!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%e!SwRSYhjsvxyz{|}!Q!R!Z!^!l#_#d#i$O$e$|%[S!QW!PT!lg%XetRSv!Q!R!l#_$e$|%[r`Oe!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%edrRSv!Q!R!l#_$e$|%[Q!UWQ!|sR#Z!PR!kfX!if!g!j#r#OZORSWYeghjsvxyz{|}!P!Q!R!Z!^!_!`!a!d!l!s#_#d#i#m#u#v#w$O$U$^$e$l$|%O%X%Y%[%eR#s!hTnQpQ$[#nQ$c#xQ$q$]R%T$rQ#n!aQ#x!sQ$_#vQ$`#wQ%P$lQ%]%OQ%c%YR%f%eQ$Z#nQ$b#xQ$n$[Q$p$]Q$z$cS%S$q$rR%_%TdtRSv!Q!R!l#_$e$|%[Q![YQ#b!ZX#e![#b#f#|vTOWe!P!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%eT!og%XT$s$_$tQ$w$_R%W$twTOWe!P!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%erVOe!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%eQ!SWQ!ujQ#OyQ#RzR#X!P#PZORSWYeghjsvxyz{|}!P!Q!R!Z!^!_!`!a!d!l!s#_#d#i#m#u#v#w$O$U$^$e$l$|%O%X%Y%[%e!WZRSYghjsvxyz{|}!Q!R!Z!^!l#_#d#i$O$e$|%X%[w[OWe!P!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%eQeOR!ee^!bb!Y#j#k#l$T$]R#o!bQ!PWQ!ZY`#W!P!Z#_#`#y$e$|%[S#_!Q!RS#`!S!XS#y#X#^Q$e#{R$|$fQ!gfR#q!gQ!jfQ#r!gT#t!j#rQpQR!zpS$U#m$^R$j$UQ$f#{R$}$fYvRS!Q!R!lR!}vQ$t$_R%U$tQ#f![Q#|#bT$Q#f#|Q#i!^Q$O#dT$R#i$OTdOeSbOeS!YW!PQ#j!_Q#k!``#l!a!s#v#w$l%O%Y%eQ#p!dU$T#m$U$^R$]#uvUOWe!P!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%edrRSv!Q!R!l#_$e$|%[Q!^YS!ng%XQ!qhQ!tjQ!|sQ#OxQ#PyQ#QzQ#S{Q#T|Q#V}Q#d!ZX#h!^#d#i$Or]Oe!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%e!WwRSYghjsvxyz{|}!Q!R!Z!^!l#_#d#i$O$e$|%X%[Q!WWR#]!P[uRSv!Q!R!lQ#{#_V${$e$|%[ToQpQ$V#mR$r$^Q!pgR%b%XraOe!_!`!a!d!s#m#u#v#w$U$^$l%O%Y%eQ!VWR#[!P",
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 operator WhileExpr keyword ConditionalOp ParenExpr IfExpr keyword FunctionCall DotGet Number PositionalArg FunctionDef Params NamedParam NamedArgPrefix String StringFragment Interpolation EscapeSeq Boolean Null colon CatchExpr keyword Block FinallyExpr keyword keyword Underscore NamedArg ElseIfExpr keyword ElseExpr FunctionCallOrIdentifier BinOp Regex Dict Array 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 Do Comment Program PipeExpr operator WhileExpr keyword ConditionalOp ParenExpr FunctionCall DotGet Number PositionalArg FunctionDef Params NamedParam NamedArgPrefix String StringFragment Interpolation EscapeSeq Boolean Null colon CatchExpr keyword Block FinallyExpr keyword keyword Underscore NamedArg IfExpr keyword FunctionCall ElseIfExpr keyword ElseExpr FunctionCallOrIdentifier BinOp Regex Dict Array FunctionCallWithBlock TryExpr keyword Throw keyword CompoundAssign Assign",
maxTerm: 109, maxTerm: 111,
context: trackScope, context: trackScope,
nodeProps: [ nodeProps: [
["closedBy", 49,"end"] ["closedBy", 47,"end"]
], ],
propSources: [highlighting], propSources: [highlighting],
skippedNodes: [0,25], skippedNodes: [0,25],
repeatNodeCount: 11, repeatNodeCount: 12,
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$QU|SOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qU|S!xYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[U|S#YQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%sW|SOp#{pq&]qt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^&dZiY|SOY&]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(VU|S!}QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(pU|S#]QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U)XW|SOt#{uw#{x!Q#{!Q![)q![#O#{#P;'S#{;'S;=`$d<%lO#{U)xY|SuQOt#{uw#{x!O#{!O!P*h!P!Q#{!Q![)q![#O#{#P;'S#{;'S;=`$d<%lO#{U*mW|SOt#{uw#{x!Q#{!Q![+V![#O#{#P;'S#{;'S;=`$d<%lO#{U+^W|SuQOt#{uw#{x!Q#{!Q![+V![#O#{#P;'S#{;'S;=`$d<%lO#{U+{^|SOt#{uw#{x}#{}!O,w!O!Q#{!Q![)q![!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{U,|[|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{U-yUzQ|SOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U.bW|SOt#{uw#{x!P#{!P!Q.z!Q#O#{#P;'S#{;'S;=`$d<%lO#{U/P^|SOY/{YZ#{Zt/{tu1Ouw/{wx1Ox!P/{!P!Q#{!Q!}/{!}#O5q#O#P3^#P;'S/{;'S;=`6r<%lO/{U0S^|S!aQOY/{YZ#{Zt/{tu1Ouw/{wx1Ox!P/{!P!Q3s!Q!}/{!}#O5q#O#P3^#P;'S/{;'S;=`6r<%lO/{Q1TX!aQOY1OZ!P1O!P!Q1p!Q!}1O!}#O2_#O#P3^#P;'S1O;'S;=`3m<%lO1OQ1sP!P!Q1vQ1{U!aQ#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;=`<%l1OU3xW|SOt#{uw#{x!P#{!P!Q4b!Q#O#{#P;'S#{;'S;=`$d<%lO#{U4ib|S!aQOt#{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[|SOY5qYZ#{Zt5qtu2_uw5qwx2_x#O5q#O#P2w#P#Q/{#Q;'S5q;'S;=`6l<%lO5qU6oP;=`<%l5qU6uP;=`<%l/{U7PU|S!RQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7jW#_Q|SOt#{uw#{x!_#{!_!`8S!`#O#{#P;'S#{;'S;=`$d<%lO#{U8XV|SOt#{uw#{x#O#{#P#Q8n#Q;'S#{;'S;=`$d<%lO#{U8uU#^Q|SOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~9^O#U~U9eU#`Q|SOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U:OU|S!YQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U:g]|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#U;`#U#o,w#o;'S#{;'S;=`$d<%lO#{U;e^|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#`,w#`#a<a#a#o,w#o;'S#{;'S;=`$d<%lO#{U<f^|SOt#{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^|SOt#{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[!PQ|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^?g[#VW|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^@d[#XW|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^Aa^#WW|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#f,w#f#gB]#g#o,w#o;'S#{;'S;=`$d<%lO#{UBb^|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#i,w#i#j=b#j#o,w#o;'S#{;'S;=`$d<%lO#{UCeUlQ|SOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~C|O#a~", tokenData: "Cx~R|OX#{XY$jYZ%TZp#{pq$jqs#{st%ntu'Vuw#{wx'[xy'ayz'zz{#{{|(e|}#{}!O(e!O!P#{!P!Q+e!Q![)S![!]4Q!]!^%T!^!}#{!}#O4k#O#P6a#P#Q6f#Q#R#{#R#S7P#S#T#{#T#Y7j#Y#Z9U#Z#b7j#b#c>r#c#f7j#f#g?u#g#h7j#h#i@x#i#o7j#o#p#{#p#qCY#q;'S#{;'S;=`$d<%l~#{~O#{~~CsS$QUzSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUzS!zYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UzS#QQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZiYzSOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mSiYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O#V~~'aO#T~U'hUzS#PQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUzS#_QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWzSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)Z[zSsQOt#{uw#{x!O#{!O!P*P!P!Q#{!Q![)S![#O#{#P#R#{#R#S(e#S;'S#{;'S;=`$d<%lO#{U*UWzSOt#{uw#{x!Q#{!Q![*n![#O#{#P;'S#{;'S;=`$d<%lO#{U*uYzSsQOt#{uw#{x!Q#{!Q![*n![#O#{#P#R#{#R#S*P#S;'S#{;'S;=`$d<%lO#{U+jWzSOt#{uw#{x!P#{!P!Q,S!Q#O#{#P;'S#{;'S;=`$d<%lO#{U,X^zSOY-TYZ#{Zt-Ttu.Wuw-Twx.Wx!P-T!P!Q#{!Q!}-T!}#O2y#O#P0f#P;'S-T;'S;=`3z<%lO-TU-[^zS!bQOY-TYZ#{Zt-Ttu.Wuw-Twx.Wx!P-T!P!Q0{!Q!}-T!}#O2y#O#P0f#P;'S-T;'S;=`3z<%lO-TQ.]X!bQOY.WZ!P.W!P!Q.x!Q!}.W!}#O/g#O#P0f#P;'S.W;'S;=`0u<%lO.WQ.{P!P!Q/OQ/TU!bQ#Z#[/O#]#^/O#a#b/O#g#h/O#i#j/O#m#n/OQ/jVOY/gZ#O/g#O#P0P#P#Q.W#Q;'S/g;'S;=`0`<%lO/gQ0SSOY/gZ;'S/g;'S;=`0`<%lO/gQ0cP;=`<%l/gQ0iSOY.WZ;'S.W;'S;=`0u<%lO.WQ0xP;=`<%l.WU1QWzSOt#{uw#{x!P#{!P!Q1j!Q#O#{#P;'S#{;'S;=`$d<%lO#{U1qbzS!bQOt#{uw#{x#O#{#P#Z#{#Z#[1j#[#]#{#]#^1j#^#a#{#a#b1j#b#g#{#g#h1j#h#i#{#i#j1j#j#m#{#m#n1j#n;'S#{;'S;=`$d<%lO#{U3O[zSOY2yYZ#{Zt2ytu/guw2ywx/gx#O2y#O#P0P#P#Q-T#Q;'S2y;'S;=`3t<%lO2yU3wP;=`<%l2yU3}P;=`<%l-TU4XUzS!PQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4rW#aQzSOt#{uw#{x!_#{!_!`5[!`#O#{#P;'S#{;'S;=`$d<%lO#{U5aVzSOt#{uw#{x#O#{#P#Q5v#Q;'S#{;'S;=`$d<%lO#{U5}U#`QzSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6fO#W~U6mU#bQzSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7WUzS!WQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7o^zSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#o7j#o;'S#{;'S;=`$d<%lO#{U8rUxQzSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U9Z_zSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#U:Y#U#o7j#o;'S#{;'S;=`$d<%lO#{U:_`zSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#`7j#`#a;a#a#o7j#o;'S#{;'S;=`$d<%lO#{U;f`zSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#g7j#g#h<h#h#o7j#o;'S#{;'S;=`$d<%lO#{U<m`zSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#X7j#X#Y=o#Y#o7j#o;'S#{;'S;=`$d<%lO#{U=v^}QzSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#o7j#o;'S#{;'S;=`$d<%lO#{^>y^#XWzSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#o7j#o;'S#{;'S;=`$d<%lO#{^?|^#ZWzSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#o7j#o;'S#{;'S;=`$d<%lO#{^AP`#YWzSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#f7j#f#gBR#g#o7j#o;'S#{;'S;=`$d<%lO#{UBW`zSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#i7j#i#j<h#j#o7j#o;'S#{;'S;=`$d<%lO#{UCaUlQzSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~CxO#c~",
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#P~~", 11)], tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#R~~", 11)],
topRules: {"Program":[0,26]}, topRules: {"Program":[0,26]},
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: 1922
}) })

View File

@ -368,6 +368,118 @@ describe('Parentheses', () => {
PositionalArg PositionalArg
Number 3`) Number 3`)
}) })
test('function call with named args on multiple lines in parens', () => {
expect(`(tail
arg1=true
arg2=30
)`).toMatchTree(`
ParenExpr
FunctionCall
Identifier tail
NamedArg
NamedArgPrefix arg1=
Boolean true
NamedArg
NamedArgPrefix arg2=
Number 30
`)
expect(`(
tail
arg1=true
arg2=30
)`).toMatchTree(`
ParenExpr
FunctionCall
Identifier tail
NamedArg
NamedArgPrefix arg1=
Boolean true
NamedArg
NamedArgPrefix arg2=
Number 30
`)
})
test('binop with newlines in parens', () => {
expect(`(
1 + 2
)`).toMatchTree(`
ParenExpr
BinOp
Number 1
Plus +
Number 2`)
})
test('comparison with newlines in parens', () => {
expect(`(
1 < 2
)`).toMatchTree(`
ParenExpr
ConditionalOp
Number 1
Lt <
Number 2`)
})
test('function call with multiple identifiers on separate lines in parens', () => {
expect(`(echo
arg1
arg2
arg3
)`).toMatchTree(`
ParenExpr
FunctionCall
Identifier echo
PositionalArg
Identifier arg1
PositionalArg
Identifier arg2
PositionalArg
Identifier arg3`)
})
})
describe('Number literals', () => {
test('allows underscores in integer literals', () => {
expect('10_000').toMatchTree(`Number 10_000`)
expect('1_000_000').toMatchTree(`Number 1_000_000`)
expect('100_000').toMatchTree(`Number 100_000`)
})
test('allows underscores in decimal literals', () => {
expect('3.14_159').toMatchTree(`Number 3.14_159`)
expect('1_000.50').toMatchTree(`Number 1_000.50`)
expect('0.000_001').toMatchTree(`Number 0.000_001`)
})
test('allows underscores in negative numbers', () => {
expect('-10_000').toMatchTree(`Number -10_000`)
expect('-3.14_159').toMatchTree(`Number -3.14_159`)
})
test('allows underscores in positive numbers with explicit sign', () => {
expect('+10_000').toMatchTree(`Number +10_000`)
expect('+3.14_159').toMatchTree(`Number +3.14_159`)
})
test('works in expressions', () => {
expect('1_000 + 2_000').toMatchTree(`
BinOp
Number 1_000
Plus +
Number 2_000`)
})
test('works in function calls', () => {
expect('echo 10_000').toMatchTree(`
FunctionCall
Identifier echo
PositionalArg
Number 10_000`)
})
}) })
describe('BinOp', () => { describe('BinOp', () => {
@ -670,11 +782,11 @@ basename = 5 # very astute
}) })
test('words with # are not considered comments', () => { test('words with # are not considered comments', () => {
expect('find #hashtag-file.txt').toMatchTree(` expect('find my#hashtag-file.txt').toMatchTree(`
FunctionCall FunctionCall
Identifier find Identifier find
PositionalArg PositionalArg
Word #hashtag-file.txt`) Word my#hashtag-file.txt`)
}) })
test('hastags in strings are not comments', () => { test('hastags in strings are not comments', () => {

View File

@ -8,9 +8,9 @@ export function specializeKeyword(ident: string) {
// tell the dotGet searcher about builtin globals // tell the dotGet searcher about builtin globals
export const globals: string[] = [] export const globals: string[] = []
export const setGlobals = (newGlobals: string[]) => { export const setGlobals = (newGlobals: string[] | Record<string, any>) => {
globals.length = 0 globals.length = 0
globals.push(...newGlobals) globals.push(...(Array.isArray(newGlobals) ? newGlobals : Object.keys(newGlobals)))
} }
// The only chars that can't be words are whitespace, apostrophes, closing parens, and EOF. // The only chars that can't be words are whitespace, apostrophes, closing parens, and EOF.

View File

@ -6,6 +6,7 @@ import {
} from 'reefvm' } from 'reefvm'
import { dict } from './dict' import { dict } from './dict'
import { json } from './json'
import { load } from './load' import { load } from './load'
import { list } from './list' import { list } from './list'
import { math } from './math' import { math } from './math'
@ -13,6 +14,7 @@ import { str } from './str'
export const globals = { export const globals = {
dict, dict,
json,
load, load,
list, list,
math, math,
@ -41,6 +43,10 @@ export const globals = {
return typeof v !== 'string' || this.scope.has(v) return typeof v !== 'string' || this.scope.has(v)
}, },
// env
args: Bun.argv.slice(1),
exit: (num: number) => process.exit(num ?? 0),
// type predicates // type predicates
'string?': (v: any) => toValue(v).type === 'string', 'string?': (v: any) => toValue(v).type === 'string',
'number?': (v: any) => toValue(v).type === 'number', 'number?': (v: any) => toValue(v).type === 'number',

7
src/prelude/json.ts Normal file
View File

@ -0,0 +1,7 @@
export const json = {
encode: (s: any) => JSON.stringify(s),
decode: (s: string) => JSON.parse(s),
}
; (json as any).parse = json.decode
; (json as any).stringify = json.encode

View File

@ -14,6 +14,13 @@ export const list = {
} }
return acc return acc
}, },
reject: async (list: any[], cb: Function) => {
let acc: any[] = []
for (const value of list) {
if (!(await cb(value))) acc.push(value)
}
return acc
},
reduce: async (list: any[], cb: Function, initial: any) => { reduce: async (list: any[], cb: Function, initial: any) => {
let acc = initial let acc = initial
for (const value of list) acc = await cb(acc, value) for (const value of list) acc = await cb(acc, value)
@ -66,6 +73,13 @@ export const list = {
const realItems = items.map(item => item.value) const realItems = items.map(item => item.value)
return toValue(realList.splice(realStart, realDeleteCount, ...realItems)) return toValue(realList.splice(realStart, realDeleteCount, ...realItems))
}, },
insert: (list: Value, index: Value, item: Value) => {
if (list.type !== 'array') return toNull()
const realList = list.value as any[]
const realIndex = index.value as number
realList.splice(realIndex, 0, item)
return toValue(realList.length)
},
// sequence operations // sequence operations
reverse: (list: any[]) => list.slice().reverse(), reverse: (list: any[]) => list.slice().reverse(),
@ -136,3 +150,4 @@ export const list = {
; (list.pop as any).raw = true ; (list.pop as any).raw = true
; (list.shift as any).raw = true ; (list.shift as any).raw = true
; (list.unshift as any).raw = true ; (list.unshift as any).raw = true
; (list.insert as any).raw = true

View File

@ -77,3 +77,17 @@ describe('introspection', () => {
await expect(`describe 'hello'`).toEvaluateTo("#<string: \u001b[32m'hello\u001b[32m'\u001b[0m>", globals) await expect(`describe 'hello'`).toEvaluateTo("#<string: \u001b[32m'hello\u001b[32m'\u001b[0m>", globals)
}) })
}) })
describe('environment', () => {
test('args is an array', async () => {
await expect(`array? args`).toEvaluateTo(true, globals)
})
test('args can be accessed', async () => {
await expect(`type args`).toEvaluateTo('array', globals)
})
test('', async () => {
await expect(`list.first args | str.ends-with? 'shrimp.test.ts'`).toEvaluateTo(true)
})
})

View File

@ -0,0 +1,84 @@
import { expect, describe, test } from 'bun:test'
describe('json', () => {
test('json.decode', () => {
expect(`json.decode '[1,2,3]'`).toEvaluateTo([1, 2, 3])
expect(`json.decode '"heya"'`).toEvaluateTo('heya')
expect(`json.decode '[true, false, null]'`).toEvaluateTo([true, false, null])
expect(`json.decode '{"a": true, "b": false, "c": "yeah"}'`).toEvaluateTo({ a: true, b: false, c: "yeah" })
})
test('json.encode', () => {
expect(`json.encode [1 2 3]`).toEvaluateTo('[1,2,3]')
expect(`json.encode 'heya'`).toEvaluateTo('"heya"')
expect(`json.encode [true false null]`).toEvaluateTo('[true,false,null]')
expect(`json.encode [a=true b=false c='yeah'] | json.decode`).toEvaluateTo({ a: true, b: false, c: "yeah" })
})
test('edge cases - empty structures', () => {
expect(`json.decode '[]'`).toEvaluateTo([])
expect(`json.decode '{}'`).toEvaluateTo({})
expect(`json.encode []`).toEvaluateTo('[]')
expect(`json.encode [=]`).toEvaluateTo('{}')
})
test('edge cases - special characters in strings', () => {
expect(`json.decode '"hello\\\\nworld"'`).toEvaluateTo('hello\nworld')
expect(`json.decode '"tab\\\\there"'`).toEvaluateTo('tab\there')
expect(`json.decode '"forward/slash"'`).toEvaluateTo('forward/slash')
expect(`json.decode '"with\\\\\\\\backslash"'`).toEvaluateTo('with\\backslash')
})
test('numbers - integers and floats', () => {
expect(`json.decode '42'`).toEvaluateTo(42)
expect(`json.decode '0'`).toEvaluateTo(0)
expect(`json.decode '-17'`).toEvaluateTo(-17)
expect(`json.decode '3.14159'`).toEvaluateTo(3.14159)
expect(`json.decode '-0.5'`).toEvaluateTo(-0.5)
})
test('numbers - scientific notation', () => {
expect(`json.decode '1e10'`).toEvaluateTo(1e10)
expect(`json.decode '2.5e-3'`).toEvaluateTo(2.5e-3)
expect(`json.decode '1.23E+5'`).toEvaluateTo(1.23e5)
})
test('unicode - emoji and special characters', () => {
expect(`json.decode '"hello 👋"'`).toEvaluateTo('hello 👋')
expect(`json.decode '"🎉🚀✨"'`).toEvaluateTo('🎉🚀✨')
expect(`json.encode '你好'`).toEvaluateTo('"你好"')
expect(`json.encode 'café'`).toEvaluateTo('"café"')
})
test('nested structures - arrays', () => {
expect(`json.decode '[[1,2],[3,4],[5,6]]'`).toEvaluateTo([[1, 2], [3, 4], [5, 6]])
expect(`json.decode '[1,[2,[3,[4]]]]'`).toEvaluateTo([1, [2, [3, [4]]]])
})
test('nested structures - objects', () => {
expect(`json.decode '{"user":{"name":"Alice","age":30}}'`).toEvaluateTo({
user: { name: 'Alice', age: 30 }
})
expect(`json.decode '{"a":{"b":{"c":"deep"}}}'`).toEvaluateTo({
a: { b: { c: 'deep' } }
})
})
test('nested structures - mixed arrays and objects', () => {
expect(`json.decode '[{"id":1,"tags":["a","b"]},{"id":2,"tags":["c"]}]'`).toEvaluateTo([
{ id: 1, tags: ['a', 'b'] },
{ id: 2, tags: ['c'] }
])
expect(`json.decode '{"items":[1,2,3],"meta":{"count":3}}'`).toEvaluateTo({
items: [1, 2, 3],
meta: { count: 3 }
})
})
test('error handling - invalid json', () => {
expect(`json.decode '{invalid}'`).toFailEvaluation()
expect(`json.decode '[1,2,3'`).toFailEvaluation()
expect(`json.decode 'undefined'`).toFailEvaluation()
expect(`json.decode ''`).toFailEvaluation()
})
})

View File

@ -66,6 +66,7 @@ describe('string operations', () => {
test('slice extracts substring', async () => { test('slice extracts substring', async () => {
await expect(`str.slice 'hello' 1 3`).toEvaluateTo('el') await expect(`str.slice 'hello' 1 3`).toEvaluateTo('el')
await expect(`str.slice 'hello' 2 null`).toEvaluateTo('llo') await expect(`str.slice 'hello' 2 null`).toEvaluateTo('llo')
await expect(`str.slice 'hello' 2`).toEvaluateTo('llo')
}) })
test('repeat repeats string', async () => { test('repeat repeats string', async () => {
@ -193,6 +194,15 @@ describe('collections', () => {
`).toEvaluateTo([3, 4, 5]) `).toEvaluateTo([3, 4, 5])
}) })
test('list.reject doesnt keep matching elements', async () => {
await expect(`
is-even = do x:
(x % 2) == 0
end
list.reject [1 2 3 4 5] is-even
`).toEvaluateTo([1, 3, 5])
})
test('list.reduce accumulates values', async () => { test('list.reduce accumulates values', async () => {
await expect(` await expect(`
add = do acc x: add = do acc x:
@ -339,6 +349,22 @@ describe('collections', () => {
await expect(`arr = [1 2 3 4 5]; list.splice arr 3 2; arr`).toEvaluateTo([1, 2, 3]) await expect(`arr = [1 2 3 4 5]; list.splice arr 3 2; arr`).toEvaluateTo([1, 2, 3])
}) })
test('list.insert adds element at index and mutates array', async () => {
await expect(`arr = [1 2 4 5]; list.insert arr 2 3; arr`).toEvaluateTo([1, 2, 3, 4, 5])
})
test('list.insert returns array length', async () => {
await expect(`list.insert [1 2 4] 2 3`).toEvaluateTo(4)
})
test('list.insert at start', async () => {
await expect(`arr = [2 3]; list.insert arr 0 1; arr`).toEvaluateTo([1, 2, 3])
})
test('list.insert at end', async () => {
await expect(`arr = [1 2]; list.insert arr 2 99; arr`).toEvaluateTo([1, 2, 99])
})
test('list.sort with no callback sorts ascending', async () => { test('list.sort with no callback sorts ascending', async () => {
await expect(`list.sort [3 1 4 1 5] null`).toEvaluateTo([1, 1, 3, 4, 5]) await expect(`list.sort [3 1 4 1 5] null`).toEvaluateTo([1, 1, 3, 4, 5])
}) })

View File

@ -1,6 +1,6 @@
import { describe } from 'bun:test' import { describe } from 'bun:test'
import { expect, test } from 'bun:test' import { expect, test } from 'bun:test'
import { Shrimp } from '..' import { Shrimp, runCode, compileCode, parseCode, bytecodeToString } from '..'
describe('Shrimp', () => { describe('Shrimp', () => {
test('allows running Shrimp code', async () => { test('allows running Shrimp code', async () => {
@ -50,4 +50,403 @@ describe('Shrimp', () => {
await shrimp.run('abc = nothing') await shrimp.run('abc = nothing')
expect(shrimp.get('abc')).toEqual('nothing') expect(shrimp.get('abc')).toEqual('nothing')
}) })
describe('set()', () => {
test('allows setting variables', async () => {
const shrimp = new Shrimp()
shrimp.set('foo', 42)
expect(shrimp.get('foo')).toEqual(42)
shrimp.set('bar', 'hello')
expect(shrimp.get('bar')).toEqual('hello')
})
test('set variables are accessible in code', async () => {
const shrimp = new Shrimp()
shrimp.set('x', 10)
shrimp.set('y', 20)
const result = await shrimp.run('x + y')
expect(result).toEqual(30)
})
test('allows setting functions', async () => {
const shrimp = new Shrimp()
shrimp.set('double', (n: number) => n * 2)
const result = await shrimp.run('double 21')
expect(result).toEqual(42)
})
test('overwrites existing variables', async () => {
const shrimp = new Shrimp()
await shrimp.run('x = 100')
expect(shrimp.get('x')).toEqual(100)
shrimp.set('x', 200)
expect(shrimp.get('x')).toEqual(200)
})
})
describe('has()', () => {
test('returns true for existing variables', async () => {
const shrimp = new Shrimp()
await shrimp.run('x = 5')
expect(shrimp.has('x')).toEqual(true)
})
test('returns false for non-existing variables', () => {
const shrimp = new Shrimp()
expect(shrimp.has('nonexistent')).toEqual(false)
})
test('returns true for globals', () => {
const shrimp = new Shrimp({ myGlobal: 42 })
expect(shrimp.has('myGlobal')).toEqual(true)
})
test('returns true for prelude functions', () => {
const shrimp = new Shrimp()
expect(shrimp.has('echo')).toEqual(true)
expect(shrimp.has('type')).toEqual(true)
})
})
describe('call()', () => {
test('calls Shrimp functions with positional args', async () => {
const shrimp = new Shrimp()
await shrimp.run(`add = do x y:
x + y
end`)
const result = await shrimp.call('add', 5, 10)
expect(result).toEqual(15)
})
test('calls Shrimp functions with named args', async () => {
const shrimp = new Shrimp()
await shrimp.run(`greet = do name:
str.join [ 'Hello ' name ] ''
end`)
const result = await shrimp.call('greet', { name: 'World' })
expect(result).toEqual('Hello World')
})
test('calls native functions', async () => {
const shrimp = new Shrimp()
shrimp.set('multiply', (a: number, b: number) => a * b)
const result = await shrimp.call('multiply', 6, 7)
expect(result).toEqual(42)
})
test('calls prelude functions', async () => {
const shrimp = new Shrimp()
const result = await shrimp.call('type', 42)
expect(result).toEqual('number')
})
test('calls async functions', async () => {
const shrimp = new Shrimp()
shrimp.set('fetchData', async () => {
return await Promise.resolve('async data')
})
const result = await shrimp.call('fetchData')
expect(result).toEqual('async data')
})
})
describe('compile()', () => {
test('compiles code to bytecode', () => {
const shrimp = new Shrimp()
const bytecode = shrimp.compile('x = 5')
expect(bytecode).toHaveProperty('instructions')
expect(bytecode).toHaveProperty('constants')
expect(bytecode).toHaveProperty('labels')
expect(bytecode.instructions.length).toBeGreaterThan(0)
})
test('respects globals when compiling', () => {
const shrimp = new Shrimp({ customGlobal: 42 })
const bytecode = shrimp.compile('x = customGlobal')
expect(bytecode.instructions.length).toBeGreaterThan(0)
})
test('compiled bytecode can be run', async () => {
const shrimp = new Shrimp()
const bytecode = shrimp.compile('2 * 21')
const result = await shrimp.run(bytecode)
expect(result).toEqual(42)
})
})
describe('parse()', () => {
test('parses code to syntax tree', () => {
const shrimp = new Shrimp()
const tree = shrimp.parse('x = 5')
expect(tree).toHaveProperty('length')
expect(tree).toHaveProperty('cursor')
expect(tree.length).toBeGreaterThan(0)
})
test('respects globals when parsing', () => {
const shrimp = new Shrimp({ myVar: 42 })
const tree = shrimp.parse('x = myVar + 10')
// Should parse without errors
expect(tree).toHaveProperty('length')
expect(tree.length).toBeGreaterThan(0)
})
test('parses function definitions', () => {
const shrimp = new Shrimp()
const tree = shrimp.parse(`add = do x y:
x + y
end`)
expect(tree.length).toBeGreaterThan(0)
})
})
describe('get()', () => {
test('returns null for undefined variables', () => {
const shrimp = new Shrimp()
expect(shrimp.get('undefined')).toEqual(null)
})
test('returns values from code execution', async () => {
const shrimp = new Shrimp()
await shrimp.run('x = 42')
expect(shrimp.get('x')).toEqual(42)
})
test('returns arrays', async () => {
const shrimp = new Shrimp()
await shrimp.run('arr = [1 2 3]')
expect(shrimp.get('arr')).toEqual([1, 2, 3])
})
test('returns dicts', async () => {
const shrimp = new Shrimp()
await shrimp.run('dict = [a=1 b=2]')
expect(shrimp.get('dict')).toEqual({ a: 1, b: 2 })
})
})
describe('running bytecode directly', () => {
test('can run pre-compiled bytecode', async () => {
const shrimp = new Shrimp()
const bytecode = shrimp.compile('x = 100')
const result = await shrimp.run(bytecode)
expect(result).toEqual(100)
expect(shrimp.get('x')).toEqual(100)
})
test('maintains state across bytecode runs', async () => {
const shrimp = new Shrimp()
const bytecode1 = shrimp.compile('x = 10')
const bytecode2 = shrimp.compile('x + 5')
await shrimp.run(bytecode1)
const result = await shrimp.run(bytecode2)
expect(result).toEqual(15)
})
})
})
describe('Functional API', () => {
describe('runCode()', () => {
test('runs code and returns result', async () => {
const result = await runCode('1 + 1')
expect(result).toEqual(2)
})
test('works with globals', async () => {
const result = await runCode('greet', { greet: () => 'hello' })
expect(result).toEqual('hello')
})
test('has access to prelude', async () => {
const result = await runCode('type 42')
expect(result).toEqual('number')
})
test('returns null for empty code', async () => {
const result = await runCode('')
expect(result).toEqual(null)
})
})
describe('compileCode()', () => {
test('compiles code to bytecode', () => {
const bytecode = compileCode('x = 5')
expect(bytecode).toHaveProperty('instructions')
expect(bytecode).toHaveProperty('constants')
expect(bytecode.instructions.length).toBeGreaterThan(0)
})
test('respects globals', () => {
const bytecode = compileCode('x = myGlobal', { myGlobal: 42 })
expect(bytecode.instructions.length).toBeGreaterThan(0)
})
test('compiled bytecode is usable', async () => {
const bytecode = compileCode('21 * 2')
const result = await runCode('21 * 2')
expect(result).toEqual(42)
})
})
describe('parseCode()', () => {
test('parses code to syntax tree', () => {
const tree = parseCode('x = 5')
expect(tree).toHaveProperty('length')
expect(tree.length).toBeGreaterThan(0)
})
test('respects globals', () => {
const tree = parseCode('x = myGlobal', { myGlobal: 42 })
expect(tree.length).toBeGreaterThan(0)
})
test('handles complex expressions', () => {
const tree = parseCode(`add = do x y:
x + y
end
result = add 5 10`)
expect(tree.length).toBeGreaterThan(0)
})
})
describe('bytecodeToString()', () => {
test('converts bytecode to human-readable format', () => {
const bytecode = compileCode('x = 42')
const str = bytecodeToString(bytecode)
expect(typeof str).toEqual('string')
expect(str.length).toBeGreaterThan(0)
})
test('shows instructions', () => {
const bytecode = compileCode('1 + 1')
const str = bytecodeToString(bytecode)
// Should contain some opcodes
expect(str).toContain('PUSH')
})
})
})
describe('Integration tests', () => {
test('complex REPL-like workflow', async () => {
const shrimp = new Shrimp()
// Define a function
await shrimp.run(`double = do x:
x * 2
end`)
expect(shrimp.has('double')).toEqual(true)
// Use the function
const result1 = await shrimp.run('double 21')
expect(result1).toEqual(42)
// Call it from TypeScript
const result2 = await shrimp.call('double', 50)
expect(result2).toEqual(100)
// Define another function using the first
await shrimp.run(`quadruple = do x:
double (double x)
end`)
const result3 = await shrimp.run('quadruple 5')
expect(result3).toEqual(20)
})
test('mixing native and Shrimp functions', async () => {
const shrimp = new Shrimp({
log: (msg: string) => `Logged: ${msg}`,
multiply: (a: number, b: number) => a * b,
})
await shrimp.run(`greet = do name:
log name
end`)
const result1 = await shrimp.run('greet Alice')
expect(result1).toEqual('Logged: Alice')
await shrimp.run(`calc = do x:
multiply x 3
end`)
const result2 = await shrimp.run('calc 7')
expect(result2).toEqual(21)
})
test('working with arrays and dicts', async () => {
const shrimp = new Shrimp()
await shrimp.run('nums = [1 2 3 4 5]')
expect(shrimp.get('nums')).toEqual([1, 2, 3, 4, 5])
await shrimp.run("config = [host='localhost' port=3000]")
expect(shrimp.get('config')).toEqual({ host: 'localhost', port: 3000 })
const result = await shrimp.run('length nums')
expect(result).toEqual(5)
})
test('compile once, run multiple times', async () => {
const bytecode = compileCode('x * 2')
const shrimp1 = new Shrimp()
shrimp1.set('x', 10)
const result1 = await shrimp1.run(bytecode)
expect(result1).toEqual(20)
const shrimp2 = new Shrimp()
shrimp2.set('x', 100)
const result2 = await shrimp2.run(bytecode)
expect(result2).toEqual(200)
})
}) })