Compare commits
9 Commits
3a1e9e9250
...
10e1986fe2
| Author | SHA1 | Date | |
|---|---|---|---|
| 10e1986fe2 | |||
| 9eaa71fe2d | |||
| f58ff1785a | |||
| 970ceeb8b0 | |||
| e2f5024a4c | |||
| 8008f37f16 | |||
| c9140bd018 | |||
| ba5ce0a88c | |||
| 398cd57b1d |
74
bin/shrimp
74
bin/shrimp
|
|
@ -2,34 +2,78 @@
|
||||||
|
|
||||||
import { colors } from '../src/prelude'
|
import { colors } from '../src/prelude'
|
||||||
import { treeToString } from '../src/utils/tree'
|
import { treeToString } from '../src/utils/tree'
|
||||||
import { runFile, compileFile, parseCode } from '../src'
|
import { runCode, runFile, compileFile, parseCode } from '../src'
|
||||||
import { bytecodeToString } from 'reefvm'
|
import { bytecodeToString } from 'reefvm'
|
||||||
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { randomUUID } from 'crypto'
|
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
${colors.bright}Usage:${colors.reset} shrimp <command> [...args]
|
${colors.bright}Usage:${colors.reset} shrimp <command> [options] [...args]
|
||||||
|
|
||||||
${colors.bright}Commands:${colors.reset}
|
${colors.bright}Commands:${colors.reset}
|
||||||
${colors.cyan}run ${colors.yellow}./my-file.sh${colors.reset} Execute a file with Shrimp
|
${colors.cyan}run ${colors.yellow}./my-file.sh${colors.reset} Execute a file with Shrimp
|
||||||
${colors.cyan}parse ${colors.yellow}./my-file.sh${colors.reset} Print parse tree for Shrimp file
|
${colors.cyan}parse ${colors.yellow}./my-file.sh${colors.reset} Print parse tree for Shrimp file
|
||||||
${colors.cyan}bytecode ${colors.yellow}./my-file.sh${colors.reset} Print bytecode for Shrimp file
|
${colors.cyan}bytecode ${colors.yellow}./my-file.sh${colors.reset} Print bytecode for Shrimp file
|
||||||
${colors.cyan}eval ${colors.yellow}'some code'${colors.reset} Evaluate a line of Shrimp code
|
${colors.cyan}eval ${colors.yellow}'some code'${colors.reset} Evaluate a line of Shrimp code
|
||||||
|
${colors.cyan}print ${colors.yellow}'some code'${colors.reset} Evaluate a line of Shrimp code and print the result
|
||||||
${colors.cyan}repl${colors.reset} Start REPL
|
${colors.cyan}repl${colors.reset} Start REPL
|
||||||
${colors.cyan}help${colors.reset} Print this help message
|
${colors.cyan}help${colors.reset} Print this help message
|
||||||
${colors.cyan}version${colors.reset} Print version`)
|
${colors.cyan}version${colors.reset} Print version
|
||||||
|
|
||||||
|
${colors.bright}Options:${colors.reset}
|
||||||
|
${colors.cyan}eval -I${colors.reset} ${colors.yellow}<module>${colors.reset} Import module (can be repeated)
|
||||||
|
Example: shrimp -I math -e 'random | echo'
|
||||||
|
Example: shrimp -Imath -Istr -e 'random | echo'`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function showVersion() {
|
function showVersion() {
|
||||||
console.log('🦐 v0.0.1')
|
console.log('🦐 v0.0.1')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function evalCode(code: string, imports: string[]) {
|
||||||
|
const importStatement = imports.length > 0 ? `import ${imports.join(' ')}` : ''
|
||||||
|
if (importStatement) code = `${importStatement}; ${code}`
|
||||||
|
return await runCode(code)
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const args = process.argv.slice(2)
|
let args = process.argv.slice(2)
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
showHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse -I flags for imports (supports both "-I math" and "-Imath")
|
||||||
|
const imports: string[] = []
|
||||||
|
|
||||||
|
while (args.length > 0) {
|
||||||
|
const arg = args[0]
|
||||||
|
|
||||||
|
if (arg === '-I') {
|
||||||
|
// "-I math" format
|
||||||
|
if (args.length < 2) {
|
||||||
|
console.log(`${colors.bright}error: -I requires a module name${colors.reset}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
imports.push(args[1])
|
||||||
|
args = args.slice(2)
|
||||||
|
} else if (arg.startsWith('-I')) {
|
||||||
|
// "-Imath" format
|
||||||
|
const moduleName = arg.slice(2)
|
||||||
|
if (!moduleName) {
|
||||||
|
console.log(`${colors.bright}error: -I requires a module name${colors.reset}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
imports.push(moduleName)
|
||||||
|
args = args.slice(1)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
showHelp()
|
showHelp()
|
||||||
|
|
@ -63,10 +107,18 @@ async function main() {
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
try { mkdirSync('/tmp/shrimp') } catch { }
|
await evalCode(code, imports)
|
||||||
const path = `/tmp/shrimp/${randomUUID()}.sh`
|
return
|
||||||
writeFileSync(path, code)
|
}
|
||||||
console.log(await runFile(path))
|
|
||||||
|
if (['print', '-print', '--print', '-E'].includes(command)) {
|
||||||
|
const code = args[1]
|
||||||
|
if (!code) {
|
||||||
|
console.log(`${colors.bright}usage: shrimp print <code>${colors.reset}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(await evalCode(code, imports))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,12 +107,12 @@ export class Compiler {
|
||||||
|
|
||||||
switch (node.type.id) {
|
switch (node.type.id) {
|
||||||
case terms.Number:
|
case terms.Number:
|
||||||
// Handle sign prefix for hex and binary literals
|
// Handle sign prefix for hex, binary, and octal literals
|
||||||
// Number() doesn't parse '-0xFF' or '+0xFF' correctly
|
// Number() doesn't parse '-0xFF', '+0xFF', '-0o77', etc. correctly
|
||||||
let numberValue: number
|
let numberValue: number
|
||||||
if (value.startsWith('-') && (value.includes('0x') || value.includes('0b'))) {
|
if (value.startsWith('-') && (value.includes('0x') || value.includes('0b') || value.includes('0o'))) {
|
||||||
numberValue = -Number(value.slice(1))
|
numberValue = -Number(value.slice(1))
|
||||||
} else if (value.startsWith('+') && (value.includes('0x') || value.includes('0b'))) {
|
} else if (value.startsWith('+') && (value.includes('0x') || value.includes('0b') || value.includes('0o'))) {
|
||||||
numberValue = Number(value.slice(1))
|
numberValue = Number(value.slice(1))
|
||||||
} else {
|
} else {
|
||||||
numberValue = Number(value)
|
numberValue = Number(value)
|
||||||
|
|
@ -797,6 +797,31 @@ export class Compiler {
|
||||||
return instructions
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case terms.Import: {
|
||||||
|
const instructions: ProgramItem[] = []
|
||||||
|
const [_import, ...nodes] = getAllChildren(node)
|
||||||
|
const args = nodes.filter(node => node.type.id === terms.Identifier)
|
||||||
|
const namedArgs = nodes.filter(node => node.type.id === terms.NamedArg)
|
||||||
|
|
||||||
|
instructions.push(['LOAD', 'import'])
|
||||||
|
|
||||||
|
args.forEach((dict) =>
|
||||||
|
instructions.push(['PUSH', input.slice(dict.from, dict.to)])
|
||||||
|
)
|
||||||
|
|
||||||
|
namedArgs.forEach((arg) => {
|
||||||
|
const { name, valueNode } = getNamedArgParts(arg, input)
|
||||||
|
instructions.push(['PUSH', name])
|
||||||
|
instructions.push(...this.#compileNode(valueNode, input))
|
||||||
|
})
|
||||||
|
|
||||||
|
instructions.push(['PUSH', args.length])
|
||||||
|
instructions.push(['PUSH', namedArgs.length])
|
||||||
|
instructions.push(['CALL'])
|
||||||
|
|
||||||
|
return instructions
|
||||||
|
}
|
||||||
|
|
||||||
case terms.Comment: {
|
case terms.Comment: {
|
||||||
return [] // ignore comments
|
return [] // ignore comments
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -448,3 +448,31 @@ describe('Compound assignment operators', () => {
|
||||||
expect('x = 10; x %= 3; x').toEvaluateTo(1)
|
expect('x = 10; x %= 3; x').toEvaluateTo(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('import', () => {
|
||||||
|
test('imports single dict', () => {
|
||||||
|
expect(`import str; starts-with? abc a`).toEvaluateTo(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('imports multiple dicts', () => {
|
||||||
|
expect(`import str math list; map [1 2 3] do x: x * 2 end`).toEvaluateTo([2, 4, 6])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('imports non-prelude dicts', () => {
|
||||||
|
expect(`
|
||||||
|
abc = [a=true b=yes c=si]
|
||||||
|
import abc
|
||||||
|
abc.b
|
||||||
|
`).toEvaluateTo('yes')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can specify imports', () => {
|
||||||
|
expect(`import str only=ends-with?; ref ends-with? | function?`).toEvaluateTo(true)
|
||||||
|
expect(`import str only=ends-with?; ref starts-with? | function?`).toEvaluateTo(false)
|
||||||
|
expect(`
|
||||||
|
abc = [a=true b=yes c=si]
|
||||||
|
import abc only=[a c]
|
||||||
|
[a c]
|
||||||
|
`).toEvaluateTo([true, 'si'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -20,6 +20,15 @@ describe('number literals', () => {
|
||||||
expect('0xABCDEF').toEvaluateTo(0xabcdef)
|
expect('0xABCDEF').toEvaluateTo(0xabcdef)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('octal literals', () => {
|
||||||
|
expect('0o644').toEvaluateTo(420)
|
||||||
|
expect('0o755').toEvaluateTo(493)
|
||||||
|
expect('0o777').toEvaluateTo(511)
|
||||||
|
expect('0o10').toEvaluateTo(8)
|
||||||
|
expect('0o0').toEvaluateTo(0)
|
||||||
|
expect('0o123').toEvaluateTo(83)
|
||||||
|
})
|
||||||
|
|
||||||
test('decimal literals still work', () => {
|
test('decimal literals still work', () => {
|
||||||
expect('42').toEvaluateTo(42)
|
expect('42').toEvaluateTo(42)
|
||||||
expect('3.14').toEvaluateTo(3.14)
|
expect('3.14').toEvaluateTo(3.14)
|
||||||
|
|
@ -27,14 +36,16 @@ describe('number literals', () => {
|
||||||
expect('999999').toEvaluateTo(999999)
|
expect('999999').toEvaluateTo(999999)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('negative hex and binary', () => {
|
test('negative hex, binary, and octal', () => {
|
||||||
expect('-0xFF').toEvaluateTo(-255)
|
expect('-0xFF').toEvaluateTo(-255)
|
||||||
expect('-0b1010').toEvaluateTo(-10)
|
expect('-0b1010').toEvaluateTo(-10)
|
||||||
|
expect('-0o755').toEvaluateTo(-493)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('positive prefix', () => {
|
test('positive prefix', () => {
|
||||||
expect('+0xFF').toEvaluateTo(255)
|
expect('+0xFF').toEvaluateTo(255)
|
||||||
expect('+0b110').toEvaluateTo(6)
|
expect('+0b110').toEvaluateTo(6)
|
||||||
|
expect('+0o644').toEvaluateTo(420)
|
||||||
expect('+42').toEvaluateTo(42)
|
expect('+42').toEvaluateTo(42)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
Number {
|
Number {
|
||||||
("-" | "+")? "0x" $[0-9a-fA-F]+ |
|
("-" | "+")? "0x" $[0-9a-fA-F]+ |
|
||||||
("-" | "+")? "0b" $[01]+ |
|
("-" | "+")? "0b" $[01]+ |
|
||||||
|
("-" | "+")? "0o" $[0-7]+ |
|
||||||
("-" | "+")? $[0-9]+ ("_"? $[0-9]+)* ('.' $[0-9]+ ("_"? $[0-9]+)*)?
|
("-" | "+")? $[0-9]+ ("_"? $[0-9]+)* ('.' $[0-9]+ ("_"? $[0-9]+)*)?
|
||||||
}
|
}
|
||||||
Boolean { "true" | "false" }
|
Boolean { "true" | "false" }
|
||||||
|
|
@ -40,6 +41,7 @@ try { @specialize[@name=keyword]<Identifier, "try"> }
|
||||||
catch { @specialize[@name=keyword]<Identifier, "catch"> }
|
catch { @specialize[@name=keyword]<Identifier, "catch"> }
|
||||||
finally { @specialize[@name=keyword]<Identifier, "finally"> }
|
finally { @specialize[@name=keyword]<Identifier, "finally"> }
|
||||||
throw { @specialize[@name=keyword]<Identifier, "throw"> }
|
throw { @specialize[@name=keyword]<Identifier, "throw"> }
|
||||||
|
import { @specialize[@name=keyword]<Identifier, "import"> }
|
||||||
null { @specialize[@name=Null]<Identifier, "null"> }
|
null { @specialize[@name=Null]<Identifier, "null"> }
|
||||||
|
|
||||||
@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot, CurlyString }
|
@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot, CurlyString }
|
||||||
|
|
@ -71,6 +73,7 @@ consumeToTerminator {
|
||||||
ambiguousFunctionCall |
|
ambiguousFunctionCall |
|
||||||
TryExpr |
|
TryExpr |
|
||||||
Throw |
|
Throw |
|
||||||
|
Import |
|
||||||
IfExpr |
|
IfExpr |
|
||||||
FunctionDef |
|
FunctionDef |
|
||||||
CompoundAssign |
|
CompoundAssign |
|
||||||
|
|
@ -160,6 +163,11 @@ Throw {
|
||||||
throw (BinOp | ConditionalOp | expression)
|
throw (BinOp | ConditionalOp | expression)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this has to be in the parse tree so the scope tracker can use it
|
||||||
|
Import {
|
||||||
|
import NamedArg* Identifier+ NamedArg*
|
||||||
|
}
|
||||||
|
|
||||||
ConditionalOp {
|
ConditionalOp {
|
||||||
expression !comparison EqEq expression |
|
expression !comparison EqEq expression |
|
||||||
expression !comparison Neq expression |
|
expression !comparison Neq expression |
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export const
|
||||||
Program = 35,
|
Program = 35,
|
||||||
PipeExpr = 36,
|
PipeExpr = 36,
|
||||||
WhileExpr = 38,
|
WhileExpr = 38,
|
||||||
keyword = 81,
|
keyword = 83,
|
||||||
ConditionalOp = 40,
|
ConditionalOp = 40,
|
||||||
ParenExpr = 41,
|
ParenExpr = 41,
|
||||||
FunctionCallWithNewlines = 42,
|
FunctionCallWithNewlines = 42,
|
||||||
|
|
@ -73,5 +73,6 @@ export const
|
||||||
FunctionCallWithBlock = 77,
|
FunctionCallWithBlock = 77,
|
||||||
TryExpr = 78,
|
TryExpr = 78,
|
||||||
Throw = 80,
|
Throw = 80,
|
||||||
CompoundAssign = 82,
|
Import = 82,
|
||||||
Assign = 83
|
CompoundAssign = 84,
|
||||||
|
Assign = 85
|
||||||
|
|
|
||||||
|
|
@ -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:78, null:112, catch:118, finally:124, end:126, if:134, else:140, try:158, throw:162}
|
const spec_Identifier = {__proto__:null,while:78, null:112, catch:118, finally:124, end:126, if:134, else:140, try:158, throw:162, import:166}
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "<zQYQbOOO!jOpO'#DXO!oOSO'#D`OOQa'#D`'#D`O%jQcO'#DvO(jQcO'#EfOOQ`'#Et'#EtO)TQRO'#DwO+YQcO'#EdO+sQbO'#DVOOQa'#Dy'#DyO.UQbO'#DzOOQa'#Ef'#EfO.]QcO'#EfO0ZQcO'#EeO1`QcO'#EdO1mQRO'#EQOOQ`'#Ed'#EdO2UQbO'#EdO2]QQO'#EcOOQ`'#Ec'#EcOOQ`'#ES'#ESQYQbOOO2hQbO'#D[O2sQbO'#DpO3nQbO'#DSO4iQQO'#D|O3nQbO'#EOO4nObO,59sO4yQbO'#DbO5RQWO'#DcOOOO'#El'#ElOOOO'#EX'#EXO5gOSO,59zOOQa,59z,59zOOQ`'#DZ'#DZO5uQbO'#DoOOQ`'#Ej'#EjOOQ`'#E['#E[O6PQbO,5:^OOQa'#Ee'#EeO3nQbO,5:cO3nQbO,5:cO3nQbO,5:cO3nQbO,5:cO3nQbO,59pO3nQbO,59pO3nQbO,59pO3nQbO,59pOOQ`'#EU'#EUO+sQbO,59qO6yQcO'#DvO7QQcO'#EfO7XQRO,59qO7cQQO,59qO7hQQO,59qO7pQQO,59qO7{QRO,59qO8eQRO,59qO8lQQO'#DQO8qQbO,5:fO8xQQO,5:eOOQa,5:f,5:fO9TQbO,5:fO9_QbO,5:mO9_QbO,5:lO:lQbO,5:gO:sQbO,59lOOQ`,5:},5:}O9_QbO'#ETOOQ`-E8Q-E8QOOQ`'#EV'#EVO;_QbO'#D]O;jQbO'#D^OOQO'#EW'#EWO;bQQO'#D]O<OQQO,59vO<TQcO'#EeO=QQRO'#EsO=}QRO'#EsOOQO'#Es'#EsO>UQQO,5:[O>ZQRO,59nO>bQRO,59nO:lQbO,5:hO>pQcO,5:jO@OQcO,5:jO@lQcO,5:jOOQa1G/_1G/_OOOO,59|,59|OOOO,59},59}OOOO-E8V-E8VOOQa1G/f1G/fOOQ`,5:Z,5:ZOOQ`-E8Y-E8YOOQa1G/}1G/}OBhQcO1G/}OBrQcO1G/}ODQQcO1G/}OD[QcO1G/}ODiQcO1G/}OOQa1G/[1G/[OEzQcO1G/[OFRQcO1G/[OFYQcO1G/[OGXQcO1G/[OFaQcO1G/[OOQ`-E8S-E8SOGoQRO1G/]OGyQQO1G/]OHOQQO1G/]OHWQQO1G/]OHcQRO1G/]OHjQRO1G/]OHqQbO,59rOH{QQO1G/]OOQa1G/]1G/]OITQQO1G0POOQa1G0Q1G0QOI`QbO1G0QOOQO'#E^'#E^OITQQO1G0POOQa1G0P1G0POOQ`'#E_'#E_OI`QbO1G0QOIjQbO1G0XOJUQbO1G0WOJpQbO'#DjOKRQbO'#DjOKfQbO1G0ROOQ`-E8R-E8ROOQ`,5:o,5:oOOQ`-E8T-E8TOKqQQO,59wOOQO,59x,59xOOQO-E8U-E8UOKyQbO1G/bO:lQbO1G/vO:lQbO1G/YOLQQbO1G0SOL]QQO7+$wOOQa7+$w7+$wOLeQQO1G/^OLmQQO7+%kOOQa7+%k7+%kOLxQbO7+%lOOQa7+%l7+%lOOQO-E8[-E8[OOQ`-E8]-E8]OOQ`'#EY'#EYOMSQQO'#EYOM[QbO'#ErOOQ`,5:U,5:UOMoQbO'#DhOMtQQO'#DkOOQ`7+%m7+%mOMyQbO7+%mONOQbO7+%mONWQbO7+$|ONfQbO7+$|ONvQbO7+%bO! OQbO7+$tOOQ`7+%n7+%nO! TQbO7+%nO! YQbO7+%nOOQa<<Hc<<HcO! bQbO7+$xO! oQQO7+$xOOQa<<IV<<IVOOQa<<IW<<IWOOQ`,5:t,5:tOOQ`-E8W-E8WO! wQQO,5:SO:lQbO,5:VOOQ`<<IX<<IXO! |QbO<<IXOOQ`<<Hh<<HhO!!RQbO<<HhO!!WQbO<<HhO!!`QbO<<HhOOQ`'#E]'#E]O!!kQbO<<H|O!!sQbO'#DuOOQ`<<H|<<H|O!!{QbO<<H|OOQ`<<H`<<H`OOQ`<<IY<<IYO!#QQbO<<IYOOQO,5:u,5:uO!#VQbO<<HdOOQO-E8X-E8XO:lQbO1G/nOOQ`1G/q1G/qOOQ`AN>sAN>sOOQ`AN>SAN>SO!#dQbOAN>SO!#iQbOAN>SOOQ`-E8Z-E8ZOOQ`AN>hAN>hO!#qQbOAN>hO2sQbO,5:_O:lQbO,5:aOOQ`AN>tAN>tPHqQbO'#EUOOQ`7+%Y7+%YOOQ`G23nG23nO!#vQbOG23nP!!vQbO'#DsOOQ`G24SG24SO!#{QQO1G/yOOQ`1G/{1G/{OOQ`LD)YLD)YO:lQbO7+%eOOQ`<<IP<<IP",
|
states: "=|QYQbOOO!mOpO'#DXO!rOSO'#D`OOQa'#D`'#D`O%mQcO'#DvO(mQcO'#EiOOQ`'#Ew'#EwO)WQRO'#DwO+]QcO'#EgO+vQbO'#DVOOQa'#Dy'#DyO.[QbO'#DzOOQa'#Ei'#EiO.cQcO'#EiO0aQcO'#EhO1fQcO'#EgO1sQRO'#ESOOQ`'#Eg'#EgO2[QbO'#EgO2cQQO'#EfOOQ`'#Ef'#EfOOQ`'#EU'#EUQYQbOOO2nQbO'#D[O2yQbO'#DpO3tQbO'#DSO4oQQO'#D|O3tQbO'#EOO4tQbO'#EQO4|ObO,59sO5XQbO'#DbO5aQWO'#DcOOOO'#Eo'#EoOOOO'#EZ'#EZO5uOSO,59zOOQa,59z,59zOOQ`'#DZ'#DZO6TQbO'#DoOOQ`'#Em'#EmOOQ`'#E^'#E^O6_QbO,5:^OOQa'#Eh'#EhO3tQbO,5:cO3tQbO,5:cO3tQbO,5:cO3tQbO,5:cO3tQbO,59pO3tQbO,59pO3tQbO,59pO3tQbO,59pOOQ`'#EW'#EWO+vQbO,59qO7XQcO'#DvO7`QcO'#EiO7gQRO,59qO7qQQO,59qO7vQQO,59qO8OQQO,59qO8ZQRO,59qO8sQRO,59qO8zQQO'#DQO9PQbO,5:fO9WQQO,5:eOOQa,5:f,5:fO9cQbO,5:fO9mQbO,5:oO9mQbO,5:nO:}QbO,5:gO;UQbO,59lOOQ`,5;Q,5;QO9mQbO'#EVOOQ`-E8S-E8SOOQ`'#EX'#EXO;pQbO'#D]O;{QbO'#D^OOQO'#EY'#EYO;sQQO'#D]O<aQQO,59vO<fQcO'#EhO=cQRO'#EvO>`QRO'#EvOOQO'#Ev'#EvO>gQQO,5:[O>lQRO,59nO>sQRO,59nO:}QbO,5:hO?RQcO,5:jO@aQcO,5:jO@}QcO,5:jOArQbO,5:lOOQ`'#Eb'#EbO4tQbO,5:lOOQa1G/_1G/_OOOO,59|,59|OOOO,59},59}OOOO-E8X-E8XOOQa1G/f1G/fOOQ`,5:Z,5:ZOOQ`-E8[-E8[OOQa1G/}1G/}OCkQcO1G/}OCuQcO1G/}OETQcO1G/}OE_QcO1G/}OElQcO1G/}OOQa1G/[1G/[OF}QcO1G/[OGUQcO1G/[OG]QcO1G/[OH[QcO1G/[OGdQcO1G/[OOQ`-E8U-E8UOHrQRO1G/]OH|QQO1G/]OIRQQO1G/]OIZQQO1G/]OIfQRO1G/]OImQRO1G/]OItQbO,59rOJOQQO1G/]OOQa1G/]1G/]OJWQQO1G0POOQa1G0Q1G0QOJcQbO1G0QOOQO'#E`'#E`OJWQQO1G0POOQa1G0P1G0POOQ`'#Ea'#EaOJcQbO1G0QOJmQbO1G0ZOKXQbO1G0YOKsQbO'#DjOLUQbO'#DjOLiQbO1G0ROOQ`-E8T-E8TOOQ`,5:q,5:qOOQ`-E8V-E8VOLtQQO,59wOOQO,59x,59xOOQO-E8W-E8WOL|QbO1G/bO:}QbO1G/vO:}QbO1G/YOMTQbO1G0SOM`QbO1G0WOM}QbO1G0WOOQ`-E8`-E8`ONUQQO7+$wOOQa7+$w7+$wON^QQO1G/^ONfQQO7+%kOOQa7+%k7+%kONqQbO7+%lOOQa7+%l7+%lOOQO-E8^-E8^OOQ`-E8_-E8_OOQ`'#E['#E[ON{QQO'#E[O! TQbO'#EuOOQ`,5:U,5:UO! hQbO'#DhO! mQQO'#DkOOQ`7+%m7+%mO! rQbO7+%mO! wQbO7+%mO!!PQbO7+$|O!!_QbO7+$|O!!oQbO7+%bO!!wQbO7+$tOOQ`7+%n7+%nO!!|QbO7+%nO!#RQbO7+%nO!#ZQbO7+%rOOQa<<Hc<<HcO!#xQbO7+$xO!$VQQO7+$xOOQa<<IV<<IVOOQa<<IW<<IWOOQ`,5:v,5:vOOQ`-E8Y-E8YO!$_QQO,5:SO:}QbO,5:VOOQ`<<IX<<IXO!$dQbO<<IXOOQ`<<Hh<<HhO!$iQbO<<HhO!$nQbO<<HhO!$vQbO<<HhOOQ`'#E_'#E_O!%RQbO<<H|O!%ZQbO'#DuOOQ`<<H|<<H|O!%cQbO<<H|OOQ`<<H`<<H`OOQ`<<IY<<IYO!%hQbO<<IYOOQO,5:w,5:wO!%mQbO<<HdOOQO-E8Z-E8ZO:}QbO1G/nOOQ`1G/q1G/qOOQ`AN>sAN>sOOQ`AN>SAN>SO!%zQbOAN>SO!&PQbOAN>SOOQ`-E8]-E8]OOQ`AN>hAN>hO!&XQbOAN>hO2yQbO,5:_O:}QbO,5:aOOQ`AN>tAN>tPItQbO'#EWOOQ`7+%Y7+%YOOQ`G23nG23nO!&^QbOG23nP!%^QbO'#DsOOQ`G24SG24SO!&cQQO1G/yOOQ`1G/{1G/{OOQ`LD)YLD)YO:}QbO7+%eOOQ`<<IP<<IP",
|
||||||
stateData: "!$T~O#UOSrOS~OlSOm`On[OoPOpROqgOwiO|[O!WRO!X[O!Y[O!ehO!l[O!qjO!skO#ZXO#[dO#_QO#jYO#kZO~O#]lO~O!ToO#_rO#amO#bnO~OlxOn[OoPOpROqgO|[O!RtO!WRO!X[O!Y[O!bsO!l[O#ZXO#_QO#jYO#kZOP#XXQ#XXR#XXS#XXT#XXU#XXW#XXX#XXY#XXZ#XX[#XX]#XX^#XXd#XXe#XXf#XXg#XXh#XXi#XXj#XXu!jX!Z!jX#i!jX~O#[!jX#m!jX!]!jX!`!jX!a!jX!h!jX~P!}OlxOn[OoPOpROqgO|[O!RtO!WRO!X[O!Y[O!bsO!l[O#ZXO#_QO#jYO#kZOP#YXQ#YXR#YXS#YXT#YXU#YXW#YXX#YXY#YXZ#YX[#YX]#YX^#YXd#YXe#YXf#YXg#YXh#YXi#YXj#YXu#YX#i#YX~O#[#YX#m#YX!Z#YX!]#YX!`#YX!a#YX!h#YX~P&QOPzOQzOR{OS{OT!OOU!POW}OX}OY}OZ}O[}O]}O^yOd|Oe|Of|Og|Oh|Oi|Oj!QO~OPzOQzOR{OS{Od|Oe|Of|Og|Oh|Oi|Ou#WX~O#[#WX#m#WX!]#WX!`#WX!a#WX#i#WX!h#WX~P*eOl!TOm`On[OoPOpROqgOwiO|[O!WRO!X[O!Y[O!ehO!l[O!qjO!skO#ZXO#[!RO#_QO#jYO#kZO~OlxOn[OoPOpRO|[O!RtO!WRO!X[O!Y[O!l[O#ZXO#[!RO#_QO#jYO#kZO~O#l!`O~P-TOV!bO#[#YX#m#YX!]#YX!`#YX!a#YX!h#YX~P'SOP#XXQ#XXR#XXS#XXT#XXU#XXW#XXX#XXY#XXZ#XX[#XX]#XX^#XXd#XXe#XXf#XXg#XXh#XXi#XXj#XXu#WX~O#[#WX#m#WX!]#WX!`#WX!a#WX#i#WX!h#WX~P.vOu#WX#[#WX#m#WX!]#WX!`#WX!a#WX#i#WX!h#WX~OT!OOU!POj!QO~P0tOV!bO_!cO`!cOa!cOb!cOc!cOk!cO~O!Z!dO~P0tOu!gO#[!fO#m!fO~Ol!iO!R!kO!Z!PP~Ol!oOn[OoPOpRO|[O!WRO!X[O!Y[O!l[O#ZXO#_QO#jYO#kZO~OlxOn[OoPOpRO|[O!WRO!X[O!Y[O!l[O#ZXO#_QO#jYO#kZO~O!Z!vO~Ol!zO|!zO#ZXO~Ol!{O#ZXO~O#_!|O#a!|O#b!|O#c!|O#d!|O#e!|O~O!ToO#_#OO#amO#bnO~OqgO!b#PO~P3nOqgO!RtO!bsOu!fa!Z!fa#[!fa#m!fa#i!fa!]!fa!`!fa!a!fa!h!fa~P3nO#[!RO~P!}O#[!RO~P&QO#[!RO#i#hO~P*eO#i#hO~O#i#hOu#WX~O!Z!dO#i#hOu#WX~O#i#hO~P.vOT!OOU!POj!QO#[!ROu#WX~O#i#hO~P8SOu!gO~O#l#jO~P-TO!RtO#[#lO#l#nO~O#[#oO#l#jO~P3nOlSOm`On[OoPOpROqgOwiO|[O!WRO!X[O!Y[O!ehO!l[O!qjO!skO#ZXO#_QO#jYO#kZO~O#[#tO~P9_Ou!gO#[ta#mta#ita!]ta!`ta!ata!hta~Ol!iO!R!kO!Z!PX~OpRO|#zO!WRO!X#zO!Y#zO#_QO~O!Z#|O~OqgO!RtO!bsOT#XXU#XXW#XXX#XXY#XXZ#XX[#XX]#XXj#XX!Z#XX~P3nOT!OOU!POj!QO!Z#gX~OT!OOU!POW}OX}OY}OZ}O[}O]}Oj!QO~O!Z#gX~P=`O!Z#}O~O!Z$OO~P=`OT!OOU!POj!QO!Z$OO~Ou!ra#[!ra#m!ra!]!ra!`!ra!a!ra#i!ra!h!ra~P)TOPzOQzOR{OS{Od|Oe|Of|Og|Oh|Oi|O~Ou!ra#[!ra#m!ra!]!ra!`!ra!a!ra#i!ra!h!ra~P?^OT!OOU!POj!QOu!ra#[!ra#m!ra!]!ra!`!ra!a!ra#i!ra!h!ra~O^yOR!kiS!kid!kie!kif!kig!kih!kii!kiu!ki#[!ki#m!ki#i!ki!]!ki!`!ki!a!ki!h!ki~OP!kiQ!ki~PAaOPzOQzO~PAaOPzOQzOd!kie!kif!kig!kih!kii!kiu!ki#[!ki#m!ki#i!ki!]!ki!`!ki!a!ki!h!ki~OR!kiS!ki~PB|OR{OS{O^yO~PB|OR{OS{O~PB|OW}OX}OY}OZ}O[}O]}OTxijxiuxi#[xi#mxi#ixi!Zxi!]xi!`xi!axi!hxi~OU!PO~PDsOU!PO~PEVOUxi~PDsOT!OOU!POjxiuxi#[xi#mxi#ixi!Zxi!]xi!`xi!axi!hxi~OW}OX}OY}OZ}O[}O]}O~PFaO#[!RO#i$RO~P*eO#i$RO~O#i$ROu#WX~O!Z!dO#i$ROu#WX~O#i$RO~P.vO#i$RO~P8SOqgO!bsO~P-TO#[!RO#i$RO~O!RtO#[#lO#l$UO~O#[#oO#l$WO~P3nOu!gO#[!ui#m!ui!]!ui!`!ui!a!ui#i!ui!h!ui~Ou!gO#[!ti#m!ti!]!ti!`!ti!a!ti#i!ti!h!ti~Ou!gO!]!^X!`!^X!a!^X!h!^X~O#[$ZO!]#fP!`#fP!a#fP!h#fP~P9_O!]$_O!`$`O!a$aO~O!R!kO!Z!Pa~O#[$eO~P9_O!]$_O!`$`O!a$hO~O#[!RO#i$kO~O#[!RO#izi~O!RtO#[#lO#l$nO~O#[#oO#l$oO~P3nOu!gO#[$pO~O#[$ZO!]#fX!`#fX!a#fX!h#fX~P9_Ol$rO~O!Z$sO~O!a$tO~O!`$`O!a$tO~Ou!gO!]$_O!`$`O!a$vO~O#[$ZO!]#fP!`#fP!a#fP~P9_O!a$}O!h$|O~O!a%PO~O!a%QO~O!`$`O!a%QO~OqgO!bsO#izq~P-TO#[!RO#izq~O!Z%VO~O!a%XO~O!a%YO~O!`$`O!a%YO~O!]$_O!`$`O!a%YO~O!a%^O!h$|O~O!Z%aO!e%`O~O!a%^O~O!a%bO~OqgO!bsO#izy~P-TO!a%eO~O!`$`O!a%eO~O!a%hO~O!a%kO~O!Z%lO~O|!l~",
|
stateData: "!&k~O#XOSrOS~OlSOm`On[OoPOpROqgOwiO|[O!WRO!X[O!Y[O!ehO!l[O!qjO!skO!ulO#^XO#_dO#bQO#mYO#nZO~O#`mO~O!TpO#bsO#dnO#eoO~OlyOn[OoPOpROqgO|[O!RuO!WRO!X[O!Y[O!btO!l[O#^XO#bQO#mYO#nZOP#[XQ#[XR#[XS#[XT#[XU#[XW#[XX#[XY#[XZ#[X[#[X]#[X^#[Xd#[Xe#[Xf#[Xg#[Xh#[Xi#[Xj#[Xu!jX!Z!jX#l!jX~O#_!jX#p!jX!]!jX!`!jX!a!jX!h!jX~P#QOlyOn[OoPOpROqgO|[O!RuO!WRO!X[O!Y[O!btO!l[O#^XO#bQO#mYO#nZOP#]XQ#]XR#]XS#]XT#]XU#]XW#]XX#]XY#]XZ#]X[#]X]#]X^#]Xd#]Xe#]Xf#]Xg#]Xh#]Xi#]Xj#]Xu#]X#l#]X~O#_#]X#p#]X!Z#]X!]#]X!`#]X!a#]X!h#]X~P&TOP{OQ{OR|OS|OT!POU!QOW!OOX!OOY!OOZ!OO[!OO]!OO^zOd}Oe}Of}Og}Oh}Oi}Oj!RO~OP{OQ{OR|OS|Od}Oe}Of}Og}Oh}Oi}Ou#ZX~O#_#ZX#p#ZX!]#ZX!`#ZX!a#ZX#l#ZX!h#ZX~P*hOl!UOm`On[OoPOpROqgOwiO|[O!WRO!X[O!Y[O!ehO!l[O!qjO!skO!ulO#^XO#_!SO#bQO#mYO#nZO~OlyOn[OoPOpRO|[O!RuO!WRO!X[O!Y[O!l[O#^XO#_!SO#bQO#mYO#nZO~O#o!aO~P-ZOV!cO#_#]X#p#]X!]#]X!`#]X!a#]X!h#]X~P'VOP#[XQ#[XR#[XS#[XT#[XU#[XW#[XX#[XY#[XZ#[X[#[X]#[X^#[Xd#[Xe#[Xf#[Xg#[Xh#[Xi#[Xj#[Xu#ZX~O#_#ZX#p#ZX!]#ZX!`#ZX!a#ZX#l#ZX!h#ZX~P.|Ou#ZX#_#ZX#p#ZX!]#ZX!`#ZX!a#ZX#l#ZX!h#ZX~OT!POU!QOj!RO~P0zOV!cO_!dO`!dOa!dOb!dOc!dOk!dO~O!Z!eO~P0zOu!hO#_!gO#p!gO~Ol!jO!R!lO!Z!PP~Ol!pOn[OoPOpRO|[O!WRO!X[O!Y[O!l[O#^XO#bQO#mYO#nZO~OlyOn[OoPOpRO|[O!WRO!X[O!Y[O!l[O#^XO#bQO#mYO#nZO~O!Z!wO~Ol!jO!RuO~Ol#OO|#OO#^XO~Ol#PO#^XO~O#b#QO#d#QO#e#QO#f#QO#g#QO#h#QO~O!TpO#b#SO#dnO#eoO~OqgO!b#TO~P3tOqgO!RuO!btOu!fa!Z!fa#_!fa#p!fa#l!fa!]!fa!`!fa!a!fa!h!fa~P3tO#_!SO~P#QO#_!SO~P&TO#_!SO#l#lO~P*hO#l#lO~O#l#lOu#ZX~O!Z!eO#l#lOu#ZX~O#l#lO~P.|OT!POU!QOj!RO#_!SOu#ZX~O#l#lO~P8bOu!hO~O#o#nO~P-ZO!RuO#_#pO#o#rO~O#_#sO#o#nO~P3tOlSOm`On[OoPOpROqgOwiO|[O!WRO!X[O!Y[O!ehO!l[O!qjO!skO!ulO#^XO#bQO#mYO#nZO~O#_#xO~P9mOu!hO#_ta#pta#lta!]ta!`ta!ata!hta~Ol!jO!R!lO!Z!PX~OpRO|$OO!WRO!X$OO!Y$OO#bQO~O!Z$QO~OqgO!RuO!btOT#[XU#[XW#[XX#[XY#[XZ#[X[#[X]#[Xj#[X!Z#[X~P3tOT!POU!QOj!RO!Z#jX~OT!POU!QOW!OOX!OOY!OOZ!OO[!OO]!OOj!RO~O!Z#jX~P=qO!Z$RO~O!Z$SO~P=qOT!POU!QOj!RO!Z$SO~Ou!ra#_!ra#p!ra!]!ra!`!ra!a!ra#l!ra!h!ra~P)WOP{OQ{OR|OS|Od}Oe}Of}Og}Oh}Oi}O~Ou!ra#_!ra#p!ra!]!ra!`!ra!a!ra#l!ra!h!ra~P?oOT!POU!QOj!ROu!ra#_!ra#p!ra!]!ra!`!ra!a!ra#l!ra!h!ra~Ol!jO!RuOu!ta#_!ta#p!ta!]!ta!`!ta!a!ta#l!ta!h!ta~O^zOR!kiS!kid!kie!kif!kig!kih!kii!kiu!ki#_!ki#p!ki#l!ki!]!ki!`!ki!a!ki!h!ki~OP!kiQ!ki~PBdOP{OQ{O~PBdOP{OQ{Od!kie!kif!kig!kih!kii!kiu!ki#_!ki#p!ki#l!ki!]!ki!`!ki!a!ki!h!ki~OR!kiS!ki~PDPOR|OS|O^zO~PDPOR|OS|O~PDPOW!OOX!OOY!OOZ!OO[!OO]!OOTxijxiuxi#_xi#pxi#lxi!Zxi!]xi!`xi!axi!hxi~OU!QO~PEvOU!QO~PFYOUxi~PEvOT!POU!QOjxiuxi#_xi#pxi#lxi!Zxi!]xi!`xi!axi!hxi~OW!OOX!OOY!OOZ!OO[!OO]!OO~PGdO#_!SO#l$YO~P*hO#l$YO~O#l$YOu#ZX~O!Z!eO#l$YOu#ZX~O#l$YO~P.|O#l$YO~P8bOqgO!btO~P-ZO#_!SO#l$YO~O!RuO#_#pO#o$]O~O#_#sO#o$_O~P3tOu!hO#_!wi#p!wi!]!wi!`!wi!a!wi#l!wi!h!wi~Ou!hO#_!vi#p!vi!]!vi!`!vi!a!vi#l!vi!h!vi~Ou!hO!]!^X!`!^X!a!^X!h!^X~O#_$bO!]#iP!`#iP!a#iP!h#iP~P9mO!]$fO!`$gO!a$hO~O!R!lO!Z!Pa~O#_$lO~P9mO!]$fO!`$gO!a$oO~O!RuOu!ti#_!ti#p!ti!]!ti!`!ti!a!ti#l!ti!h!ti~Ol!jO~PM`O#_!SO#l$sO~O#_!SO#lzi~O!RuO#_#pO#o$vO~O#_#sO#o$wO~P3tOu!hO#_$xO~O#_$bO!]#iX!`#iX!a#iX!h#iX~P9mOl$zO~O!Z${O~O!a$|O~O!`$gO!a$|O~Ou!hO!]$fO!`$gO!a%OO~O#_$bO!]#iP!`#iP!a#iP~P9mO!a%VO!h%UO~O!a%XO~O!a%YO~O!`$gO!a%YO~O!RuOu!tq#_!tq#p!tq!]!tq!`!tq!a!tq#l!tq!h!tq~OqgO!btO#lzq~P-ZO#_!SO#lzq~O!Z%_O~O!a%aO~O!a%bO~O!`$gO!a%bO~O!]$fO!`$gO!a%bO~O!a%fO!h%UO~O!Z%iO!e%hO~O!a%fO~O!a%jO~OqgO!btO#lzy~P-ZO!a%mO~O!`$gO!a%mO~O!a%pO~O!a%sO~O!Z%tO~O|!l~",
|
||||||
goto: "8h#iPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP#jP$TP$j%h&v&|P(W(d)^)aP)gP*n*nPPPP*rP+O+hPPP,O#jP,h-RP-V-]-rP.i/m$T$TP$TP$T$T0s0y1V1y2P2Z2a2h2n2x3O3YPPP3d3h4]6RPPP7]P7mPPPPP7q7w7}raOf!b!c!d!g!v#t#|#}$O$]$e$s%V%a%lQ!XXR#b!SwaOXf!S!b!c!d!g!v#t#|#}$O$]$e$s%V%a%lr_Of!b!c!d!g!v#t#|#}$O$]$e$s%V%a%lQ![XS!ph%`Q!uiQ!ykQ#X!PQ#Z!OQ#^!QR#e!SvTOfh!b!c!d!g!v#t#|#}$O$]$e$s%V%`%a%l!W[STZiktwyz{|}!O!P!Q!T!U!^!a!o#f#k#p$V$l%T%cS!UX!SQ!zlR!{mQ!WXR#a!SrSOf!b!c!d!g!v#t#|#}$O$]$e$s%V%a%l!WxSTZiktwyz{|}!O!P!Q!T!U!^!a!o#f#k#p$V$l%T%cS!TX!ST!oh%`euSTw!T!U!o#f$l%T%craOf!b!c!d!g!v#t#|#}$O$]$e$s%V%a%ldsSTw!T!U!o#f$l%T%cQ!XXQ#PtR#b!SR!ngX!lg!j!m#y#S[OSTXZfhiktwyz{|}!O!P!Q!S!T!U!^!a!b!c!d!g!o!v#f#k#p#t#|#}$O$V$]$e$l$s%T%V%`%a%c%lR#z!kToQqQ$c#uQ$j$PQ$x$dR%[$yQ#u!dQ$P!vQ$f#}Q$g$OQ%W$sQ%d%VQ%j%aR%m%lQ$b#uQ$i$PQ$u$cQ$w$dQ%R$jS%Z$x$yR%f%[duSTw!T!U!o#f$l%T%cQ!_ZQ#i!^X#l!_#i#m$TvUOXf!S!b!c!d!g!v#t#|#}$O$]$e$s%V%a%lT!rh%`T$z$f${Q%O$fR%_${wUOXf!S!b!c!d!g!v#t#|#}$O$]$e$s%V%a%lrWOf!b!c!d!g!v#t#|#}$O$]$e$s%V%a%lQ!VXQ!xkQ#RzQ#U{Q#W|R#`!S#T[OSTXZfhiktwyz{|}!O!P!Q!S!T!U!^!a!b!c!d!g!o!v#f#k#p#t#|#}$O$V$]$e$l$s%T%V%`%a%c%l![[STZhiktwyz{|}!O!P!Q!T!U!^!a!o#f#k#p$V$l%T%`%cw]OXf!S!b!c!d!g!v#t#|#}$O$]$e$s%V%a%lQfOR!hf^!ec!]#q#r#s$[$dR#v!eQ!SXQ!^Z`#_!S!^#f#g$Q$l%T%cS#f!T!US#g!V![S$Q#`#eQ$l$SR%T$mQ!jgR#x!jQ!mgQ#y!jT#{!m#yQqQR!}qS$]#t$eR$q$]Q$m$SR%U$mYwST!T!U!oR#QwQ${$fR%]${Q#m!_Q$T#iT$X#m$TQ#p!aQ$V#kT$Y#p$VTeOfScOfS!]X!SQ#q!bQ#r!c`#s!d!v#}$O$s%V%a%lQ#w!gU$[#t$]$eR$d#|vVOXf!S!b!c!d!g!v#t#|#}$O$]$e$s%V%a%ldsSTw!T!U!o#f$l%T%cQ!aZS!qh%`Q!tiQ!wkQ#PtQ#RyQ#SzQ#T{Q#V|Q#X}Q#Y!OQ#[!PQ#]!QQ#k!^X#o!a#k#p$Vr^Of!b!c!d!g!v#t#|#}$O$]$e$s%V%a%l![xSTZhiktwyz{|}!O!P!Q!T!U!^!a!o#f#k#p$V$l%T%`%cQ!ZXR#d!S[vSTw!T!U!oQ$S#fV%S$l%T%cTpQqQ$^#tR$y$eQ!shR%i%`rbOf!b!c!d!g!v#t#|#}$O$]$e$s%V%a%lQ!YXR#c!S",
|
goto: "9Z#lPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP#mP$WP$m%k&y'PP(Z(g)a)dP)jP*q*qPPPP*uP+R+kPPP,R#mP,s-^P-b-h-}P.t/x$W$WP$WP$WP$W$W1O1U1b2U2d2n2t2{3R3]3c3m3wPPP4V4Z5O6tPPP8OP8`PPPPP8d8j8praOf!c!d!e!h!w#x$Q$R$S$d$l${%_%i%tQ!YXR#f!TwaOXf!T!c!d!e!h!w#x$Q$R$S$d$l${%_%i%tr_Of!c!d!e!h!w#x$Q$R$S$d$l${%_%i%tQ!]XS!qh%hQ!viQ!zkQ#]!QQ#_!PQ#b!RR#i!TvTOfh!c!d!e!h!w#x$Q$R$S$d$l${%_%h%i%t!W[STZikuxz{|}!O!P!Q!R!U!V!_!b!p#j#o#t$^$t%]%kS!VX!TQ#OmR#PnQ!XXR#e!TrSOf!c!d!e!h!w#x$Q$R$S$d$l${%_%i%t!WySTZikuxz{|}!O!P!Q!R!U!V!_!b!p#j#o#t$^$t%]%kS!UX!TT!ph%hevSTx!U!V!p#j$t%]%kraOf!c!d!e!h!w#x$Q$R$S$d$l${%_%i%tdtSTx!U!V!p#j$t%]%kQ!YXQ#TuR#f!TR!ogX!mg!k!n#}#S[OSTXZfhikuxz{|}!O!P!Q!R!T!U!V!_!b!c!d!e!h!p!w#j#o#t#x$Q$R$S$^$d$l$t${%]%_%h%i%k%tR$O!lTpQrQ$j#yQ$q$TQ%Q$kR%d%RQ#y!eQ$T!wQ$m$RQ$n$SQ%`${Q%l%_Q%r%iR%u%tQ$i#yQ$p$TQ$}$jQ%P$kQ%Z$qS%c%Q%RR%n%ddvSTx!U!V!p#j$t%]%kQ!`Z[!|l!{!}$U$V$rQ#m!_X#p!`#m#q$[vUOXf!T!c!d!e!h!w#x$Q$R$S$d$l${%_%i%tT!sh%hT%S$m%TQ%W$mR%g%TwUOXf!T!c!d!e!h!w#x$Q$R$S$d$l${%_%i%trWOf!c!d!e!h!w#x$Q$R$S$d$l${%_%i%tQ!WXQ!ykQ#V{Q#Y|Q#[}R#d!T#T[OSTXZfhikuxz{|}!O!P!Q!R!T!U!V!_!b!c!d!e!h!p!w#j#o#t#x$Q$R$S$^$d$l$t${%]%_%h%i%k%t![[STZhikuxz{|}!O!P!Q!R!U!V!_!b!p#j#o#t$^$t%]%h%kw]OXf!T!c!d!e!h!w#x$Q$R$S$d$l${%_%i%tQfOR!if^!fc!^#u#v#w$c$kR#z!fQ!TXQ!_Z`#c!T!_#j#k$X$t%]%kS#j!U!VS#k!W!]S$X#d#iQ$t$ZR%]$uQ!kgQ!{lU#|!k!{$VR$V!}Q!ngQ#}!kT$P!n#}QrQR#RrS$d#x$lR$y$dQ$u$ZR%^$uYxST!U!V!pR#UxQ%T$mR%e%TQ#q!`Q$[#mT$`#q$[Q#t!bQ$^#oT$a#t$^Q!}lQ$U!{U$W!}$U$rR$r$VTeOfScOfS!^X!TQ#u!cQ#v!d`#w!e!w$R$S${%_%i%tQ#{!hU$c#x$d$lR$k$QvVOXf!T!c!d!e!h!w#x$Q$R$S$d$l${%_%i%tdtSTx!U!V!p#j$t%]%kQ!bZS!rh%hQ!uiQ!xkQ#TuQ#VzQ#W{Q#X|Q#Z}Q#]!OQ#^!PQ#`!QQ#a!RQ#o!_X#s!b#o#t$^r^Of!c!d!e!h!w#x$Q$R$S$d$l${%_%i%t![ySTZhikuxz{|}!O!P!Q!R!U!V!_!b!p#j#o#t$^$t%]%h%kQ![XR#h!T[wSTx!U!V!pQ$Z#jV%[$t%]%kTqQrQ$e#xR%R$lQ!thR%q%hrbOf!c!d!e!h!w#x$Q$R$S$d$l${%_%i%tQ!ZXR#g!T",
|
||||||
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo PlusEq MinusEq StarEq SlashEq ModuloEq Band Bor Bxor Shl Shr Ushr NullishCoalesce NullishEq Identifier AssignableIdentifier Word IdentifierBeforeDot CurlyString Do Comment Program PipeExpr operator WhileExpr keyword ConditionalOp ParenExpr FunctionCall DotGet Number PositionalArg FunctionDef Params NamedParam NamedArgPrefix String StringFragment Interpolation EscapeSeq DoubleQuote 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",
|
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo PlusEq MinusEq StarEq SlashEq ModuloEq Band Bor Bxor Shl Shr Ushr NullishCoalesce NullishEq Identifier AssignableIdentifier Word IdentifierBeforeDot CurlyString Do Comment Program PipeExpr operator WhileExpr keyword ConditionalOp ParenExpr FunctionCall DotGet Number PositionalArg FunctionDef Params NamedParam NamedArgPrefix String StringFragment Interpolation EscapeSeq DoubleQuote 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 Import keyword CompoundAssign Assign",
|
||||||
maxTerm: 121,
|
maxTerm: 124,
|
||||||
context: trackScope,
|
context: trackScope,
|
||||||
nodeProps: [
|
nodeProps: [
|
||||||
["closedBy", 57,"end"]
|
["closedBy", 57,"end"]
|
||||||
],
|
],
|
||||||
propSources: [highlighting],
|
propSources: [highlighting],
|
||||||
skippedNodes: [0,34],
|
skippedNodes: [0,34],
|
||||||
repeatNodeCount: 12,
|
repeatNodeCount: 13,
|
||||||
tokenData: "K]~R!OOX$RXY$pYZ%ZZp$Rpq$pqr$Rrs%tst'ztu)cuw$Rwx)hxy)myz*Wz{$R{|*q|}$R}!O*q!O!P$R!P!Q2x!Q!R+c!R![.Q![!];e!]!^%Z!^!}$R!}#O<O#O#P=t#P#Q=y#Q#R$R#R#S>d#S#T$R#T#Y>}#Y#Z@i#Z#b>}#b#cFV#c#f>}#f#gGY#g#h>}#h#iH]#i#o>}#o#p$R#p#qJm#q;'S$R;'S;=`$j<%l~$R~O$R~~KWS$WU!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RS$mP;=`<%l$R^$wU!TS#UYOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU%bU!TS#[QOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU%yZ!TSOr%trs&lst%ttu'Vuw%twx'Vx#O%t#O#P'V#P;'S%t;'S;=`'t<%lO%tU&sU!WQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RQ'YTOr'Vrs'is;'S'V;'S;=`'n<%lO'VQ'nO!WQQ'qP;=`<%l'VU'wP;=`<%l%t^(RZrY!TSOY'zYZ$RZt'ztu(tuw'zwx(tx#O'z#O#P(t#P;'S'z;'S;=`)]<%lO'zY(ySrYOY(tZ;'S(t;'S;=`)V<%lO(tY)YP;=`<%l(t^)`P;=`<%l'z~)hO#a~~)mO#_~U)tU!TS#ZQOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU*_U!TS#iQOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU*vX!TSOt$Ruw$Rx!Q$R!Q!R+c!R![.Q![#O$R#P;'S$R;'S;=`$j<%lO$RU+j`!TS|QOt$Ruw$Rx!O$R!O!P,l!P!Q$R!Q![.Q![#O$R#P#R$R#R#S.}#S#U$R#U#V/l#V#l$R#l#m1Q#m;'S$R;'S;=`$j<%lO$RU,qW!TSOt$Ruw$Rx!Q$R!Q![-Z![#O$R#P;'S$R;'S;=`$j<%lO$RU-bY!TS|QOt$Ruw$Rx!Q$R!Q![-Z![#O$R#P#R$R#R#S,l#S;'S$R;'S;=`$j<%lO$RU.X[!TS|QOt$Ruw$Rx!O$R!O!P,l!P!Q$R!Q![.Q![#O$R#P#R$R#R#S.}#S;'S$R;'S;=`$j<%lO$RU/SW!TSOt$Ruw$Rx!Q$R!Q![.Q![#O$R#P;'S$R;'S;=`$j<%lO$RU/qX!TSOt$Ruw$Rx!Q$R!Q!R0^!R!S0^!S#O$R#P;'S$R;'S;=`$j<%lO$RU0eX!TS|QOt$Ruw$Rx!Q$R!Q!R0^!R!S0^!S#O$R#P;'S$R;'S;=`$j<%lO$RU1V[!TSOt$Ruw$Rx!Q$R!Q![1{![!c$R!c!i1{!i#O$R#P#T$R#T#Z1{#Z;'S$R;'S;=`$j<%lO$RU2S[!TS|QOt$Ruw$Rx!Q$R!Q![1{![!c$R!c!i1{!i#O$R#P#T$R#T#Z1{#Z;'S$R;'S;=`$j<%lO$RU2}W!TSOt$Ruw$Rx!P$R!P!Q3g!Q#O$R#P;'S$R;'S;=`$j<%lO$RU3l^!TSOY4hYZ$RZt4htu5kuw4hwx5kx!P4h!P!Q$R!Q!}4h!}#O:^#O#P7y#P;'S4h;'S;=`;_<%lO4hU4o^!TS!lQOY4hYZ$RZt4htu5kuw4hwx5kx!P4h!P!Q8`!Q!}4h!}#O:^#O#P7y#P;'S4h;'S;=`;_<%lO4hQ5pX!lQOY5kZ!P5k!P!Q6]!Q!}5k!}#O6z#O#P7y#P;'S5k;'S;=`8Y<%lO5kQ6`P!P!Q6cQ6hU!lQ#Z#[6c#]#^6c#a#b6c#g#h6c#i#j6c#m#n6cQ6}VOY6zZ#O6z#O#P7d#P#Q5k#Q;'S6z;'S;=`7s<%lO6zQ7gSOY6zZ;'S6z;'S;=`7s<%lO6zQ7vP;=`<%l6zQ7|SOY5kZ;'S5k;'S;=`8Y<%lO5kQ8]P;=`<%l5kU8eW!TSOt$Ruw$Rx!P$R!P!Q8}!Q#O$R#P;'S$R;'S;=`$j<%lO$RU9Ub!TS!lQOt$Ruw$Rx#O$R#P#Z$R#Z#[8}#[#]$R#]#^8}#^#a$R#a#b8}#b#g$R#g#h8}#h#i$R#i#j8}#j#m$R#m#n8}#n;'S$R;'S;=`$j<%lO$RU:c[!TSOY:^YZ$RZt:^tu6zuw:^wx6zx#O:^#O#P7d#P#Q4h#Q;'S:^;'S;=`;X<%lO:^U;[P;=`<%l:^U;bP;=`<%l4hU;lU!TS!ZQOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU<VW#kQ!TSOt$Ruw$Rx!_$R!_!`<o!`#O$R#P;'S$R;'S;=`$j<%lO$RU<tV!TSOt$Ruw$Rx#O$R#P#Q=Z#Q;'S$R;'S;=`$j<%lO$RU=bU#jQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$R~=yO#b~U>QU#lQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU>kU!TS!bQOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU?S^!TSOt$Ruw$Rx}$R}!O>}!O!Q$R!Q![>}![!_$R!_!`@O!`#O$R#P#T$R#T#o>}#o;'S$R;'S;=`$j<%lO$RU@VU!RQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU@n_!TSOt$Ruw$Rx}$R}!O>}!O!Q$R!Q![>}![!_$R!_!`@O!`#O$R#P#T$R#T#UAm#U#o>}#o;'S$R;'S;=`$j<%lO$RUAr`!TSOt$Ruw$Rx}$R}!O>}!O!Q$R!Q![>}![!_$R!_!`@O!`#O$R#P#T$R#T#`>}#`#aBt#a#o>}#o;'S$R;'S;=`$j<%lO$RUBy`!TSOt$Ruw$Rx}$R}!O>}!O!Q$R!Q![>}![!_$R!_!`@O!`#O$R#P#T$R#T#g>}#g#hC{#h#o>}#o;'S$R;'S;=`$j<%lO$RUDQ`!TSOt$Ruw$Rx}$R}!O>}!O!Q$R!Q![>}![!_$R!_!`@O!`#O$R#P#T$R#T#X>}#X#YES#Y#o>}#o;'S$R;'S;=`$j<%lO$RUEZ^!XQ!TSOt$Ruw$Rx}$R}!O>}!O!Q$R!Q![>}![!_$R!_!`@O!`#O$R#P#T$R#T#o>}#o;'S$R;'S;=`$j<%lO$R^F^^#cW!TSOt$Ruw$Rx}$R}!O>}!O!Q$R!Q![>}![!_$R!_!`@O!`#O$R#P#T$R#T#o>}#o;'S$R;'S;=`$j<%lO$R^Ga^#eW!TSOt$Ruw$Rx}$R}!O>}!O!Q$R!Q![>}![!_$R!_!`@O!`#O$R#P#T$R#T#o>}#o;'S$R;'S;=`$j<%lO$R^Hd`#dW!TSOt$Ruw$Rx}$R}!O>}!O!Q$R!Q![>}![!_$R!_!`@O!`#O$R#P#T$R#T#f>}#f#gIf#g#o>}#o;'S$R;'S;=`$j<%lO$RUIk`!TSOt$Ruw$Rx}$R}!O>}!O!Q$R!Q![>}![!_$R!_!`@O!`#O$R#P#T$R#T#i>}#i#jC{#j#o>}#o;'S$R;'S;=`$j<%lO$RUJtUuQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$R~K]O#m~",
|
tokenData: "Lq~R!OOX$RXY$pYZ%ZZp$Rpq$pqr$Rrs%tst'ztu)cuw$Rwx)hxy)myz*Wz{$R{|*q|}$R}!O*q!O!P$R!P!Q4^!Q!R+c!R![.W![!]<y!]!^%Z!^!}$R!}#O=d#O#P?Y#P#Q?_#Q#R$R#R#S?x#S#T$R#T#Y@c#Y#ZA}#Z#b@c#b#cGk#c#f@c#f#gHn#g#h@c#h#iIq#i#o@c#o#p$R#p#qLR#q;'S$R;'S;=`$j<%l~$R~O$R~~LlS$WU!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RS$mP;=`<%l$R^$wU!TS#XYOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU%bU!TS#_QOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU%yZ!TSOr%trs&lst%ttu'Vuw%twx'Vx#O%t#O#P'V#P;'S%t;'S;=`'t<%lO%tU&sU!WQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RQ'YTOr'Vrs'is;'S'V;'S;=`'n<%lO'VQ'nO!WQQ'qP;=`<%l'VU'wP;=`<%l%t^(RZrY!TSOY'zYZ$RZt'ztu(tuw'zwx(tx#O'z#O#P(t#P;'S'z;'S;=`)]<%lO'zY(ySrYOY(tZ;'S(t;'S;=`)V<%lO(tY)YP;=`<%l(t^)`P;=`<%l'z~)hO#d~~)mO#b~U)tU!TS#^QOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU*_U!TS#lQOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU*vX!TSOt$Ruw$Rx!Q$R!Q!R+c!R![.W![#O$R#P;'S$R;'S;=`$j<%lO$RU+jb!TS|QOt$Ruw$Rx!O$R!O!P,r!P!Q$R!Q![.W![#O$R#P#R$R#R#S/T#S#U$R#U#V/r#V#c$R#c#d1W#d#l$R#l#m2f#m;'S$R;'S;=`$j<%lO$RU,wW!TSOt$Ruw$Rx!Q$R!Q![-a![#O$R#P;'S$R;'S;=`$j<%lO$RU-hY!TS|QOt$Ruw$Rx!Q$R!Q![-a![#O$R#P#R$R#R#S,r#S;'S$R;'S;=`$j<%lO$RU._[!TS|QOt$Ruw$Rx!O$R!O!P,r!P!Q$R!Q![.W![#O$R#P#R$R#R#S/T#S;'S$R;'S;=`$j<%lO$RU/YW!TSOt$Ruw$Rx!Q$R!Q![.W![#O$R#P;'S$R;'S;=`$j<%lO$RU/wX!TSOt$Ruw$Rx!Q$R!Q!R0d!R!S0d!S#O$R#P;'S$R;'S;=`$j<%lO$RU0kX!TS|QOt$Ruw$Rx!Q$R!Q!R0d!R!S0d!S#O$R#P;'S$R;'S;=`$j<%lO$RU1]W!TSOt$Ruw$Rx!Q$R!Q!Y1u!Y#O$R#P;'S$R;'S;=`$j<%lO$RU1|W!TS|QOt$Ruw$Rx!Q$R!Q!Y1u!Y#O$R#P;'S$R;'S;=`$j<%lO$RU2k[!TSOt$Ruw$Rx!Q$R!Q![3a![!c$R!c!i3a!i#O$R#P#T$R#T#Z3a#Z;'S$R;'S;=`$j<%lO$RU3h[!TS|QOt$Ruw$Rx!Q$R!Q![3a![!c$R!c!i3a!i#O$R#P#T$R#T#Z3a#Z;'S$R;'S;=`$j<%lO$RU4cW!TSOt$Ruw$Rx!P$R!P!Q4{!Q#O$R#P;'S$R;'S;=`$j<%lO$RU5Q^!TSOY5|YZ$RZt5|tu7Puw5|wx7Px!P5|!P!Q$R!Q!}5|!}#O;r#O#P9_#P;'S5|;'S;=`<s<%lO5|U6T^!TS!lQOY5|YZ$RZt5|tu7Puw5|wx7Px!P5|!P!Q9t!Q!}5|!}#O;r#O#P9_#P;'S5|;'S;=`<s<%lO5|Q7UX!lQOY7PZ!P7P!P!Q7q!Q!}7P!}#O8`#O#P9_#P;'S7P;'S;=`9n<%lO7PQ7tP!P!Q7wQ7|U!lQ#Z#[7w#]#^7w#a#b7w#g#h7w#i#j7w#m#n7wQ8cVOY8`Z#O8`#O#P8x#P#Q7P#Q;'S8`;'S;=`9X<%lO8`Q8{SOY8`Z;'S8`;'S;=`9X<%lO8`Q9[P;=`<%l8`Q9bSOY7PZ;'S7P;'S;=`9n<%lO7PQ9qP;=`<%l7PU9yW!TSOt$Ruw$Rx!P$R!P!Q:c!Q#O$R#P;'S$R;'S;=`$j<%lO$RU:jb!TS!lQOt$Ruw$Rx#O$R#P#Z$R#Z#[:c#[#]$R#]#^:c#^#a$R#a#b:c#b#g$R#g#h:c#h#i$R#i#j:c#j#m$R#m#n:c#n;'S$R;'S;=`$j<%lO$RU;w[!TSOY;rYZ$RZt;rtu8`uw;rwx8`x#O;r#O#P8x#P#Q5|#Q;'S;r;'S;=`<m<%lO;rU<pP;=`<%l;rU<vP;=`<%l5|U=QU!TS!ZQOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU=kW#nQ!TSOt$Ruw$Rx!_$R!_!`>T!`#O$R#P;'S$R;'S;=`$j<%lO$RU>YV!TSOt$Ruw$Rx#O$R#P#Q>o#Q;'S$R;'S;=`$j<%lO$RU>vU#mQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$R~?_O#e~U?fU#oQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU@PU!TS!bQOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU@h^!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$RUAkU!RQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RUBS_!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#UCR#U#o@c#o;'S$R;'S;=`$j<%lO$RUCW`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#`@c#`#aDY#a#o@c#o;'S$R;'S;=`$j<%lO$RUD_`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#g@c#g#hEa#h#o@c#o;'S$R;'S;=`$j<%lO$RUEf`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#X@c#X#YFh#Y#o@c#o;'S$R;'S;=`$j<%lO$RUFo^!XQ!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$R^Gr^#fW!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$R^Hu^#hW!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$R^Ix`#gW!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#f@c#f#gJz#g#o@c#o;'S$R;'S;=`$j<%lO$RUKP`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#i@c#i#jEa#j#o@c#o;'S$R;'S;=`$j<%lO$RULYUuQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$R~LqO#p~",
|
||||||
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#`~~", 11)],
|
||||||
topRules: {"Program":[0,35]},
|
topRules: {"Program":[0,35]},
|
||||||
specialized: [{term: 28, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 28, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
|
specialized: [{term: 28, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 28, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
|
||||||
tokenPrec: 2256
|
tokenPrec: 2370
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1017,3 +1017,34 @@ Assign
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('import', () => {
|
||||||
|
test('parses single import', () => {
|
||||||
|
expect(`import str`).toMatchTree(`
|
||||||
|
Import
|
||||||
|
keyword import
|
||||||
|
Identifier str
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses multiple imports', () => {
|
||||||
|
expect(`import str math list`).toMatchTree(`
|
||||||
|
Import
|
||||||
|
keyword import
|
||||||
|
Identifier str
|
||||||
|
Identifier math
|
||||||
|
Identifier list
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses named args', () => {
|
||||||
|
expect(`import str only=ends-with?`).toMatchTree(`
|
||||||
|
Import
|
||||||
|
keyword import
|
||||||
|
Identifier str
|
||||||
|
NamedArg
|
||||||
|
NamedArgPrefix only=
|
||||||
|
Identifier ends-with?
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -21,6 +21,16 @@ describe('number literals', () => {
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('octal numbers', () => {
|
||||||
|
expect('0o644').toMatchTree(`
|
||||||
|
Number 0o644
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect('0o055').toMatchTree(`
|
||||||
|
Number 0o055
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
test('decimal numbers still work', () => {
|
test('decimal numbers still work', () => {
|
||||||
expect('42').toMatchTree(`
|
expect('42').toMatchTree(`
|
||||||
Number 42
|
Number 42
|
||||||
|
|
@ -39,6 +49,12 @@ describe('number literals', () => {
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('negative octal', () => {
|
||||||
|
expect('-0o755').toMatchTree(`
|
||||||
|
Number -0o755
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
test('positive prefix binary', () => {
|
test('positive prefix binary', () => {
|
||||||
expect('+0b110').toMatchTree(`
|
expect('+0b110').toMatchTree(`
|
||||||
Number +0b110
|
Number +0b110
|
||||||
|
|
@ -51,11 +67,18 @@ describe('number literals', () => {
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('hex and binary in arrays', () => {
|
test('positive prefix octal', () => {
|
||||||
expect('[0xFF 0b110 42]').toMatchTree(`
|
expect('+0o644').toMatchTree(`
|
||||||
|
Number +0o644
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hex, binary, and octal in arrays', () => {
|
||||||
|
expect('[0xFF 0b110 0o644 42]').toMatchTree(`
|
||||||
Array
|
Array
|
||||||
Number 0xFF
|
Number 0xFF
|
||||||
Number 0b110
|
Number 0b110
|
||||||
|
Number 0o644
|
||||||
Number 42
|
Number 42
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { type Value, toString, toValue } from 'reefvm'
|
import { type Value, toString } from 'reefvm'
|
||||||
|
|
||||||
export const dict = {
|
export const dict = {
|
||||||
keys: (dict: Record<string, any>) => Object.keys(dict),
|
keys: (dict: Record<string, any>) => Object.keys(dict),
|
||||||
|
|
|
||||||
128
src/prelude/fs.ts
Normal file
128
src/prelude/fs.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { join, resolve, basename, dirname, extname } from 'path'
|
||||||
|
import {
|
||||||
|
readdirSync, mkdirSync, rmdirSync,
|
||||||
|
readFileSync, writeFileSync, appendFileSync,
|
||||||
|
rmSync, copyFileSync,
|
||||||
|
statSync, lstatSync, chmodSync, symlinkSync, readlinkSync,
|
||||||
|
watch
|
||||||
|
} from "fs"
|
||||||
|
|
||||||
|
export const fs = {
|
||||||
|
// Directory operations
|
||||||
|
ls: (path: string) => readdirSync(path),
|
||||||
|
mkdir: (path: string) => mkdirSync(path, { recursive: true }),
|
||||||
|
rmdir: (path: string) => rmdirSync(path === '/' || path === '' ? '/tmp/*' : path, { recursive: true }),
|
||||||
|
pwd: () => process.cwd(),
|
||||||
|
cd: (path: string) => process.chdir(path),
|
||||||
|
|
||||||
|
// Reading
|
||||||
|
read: (path: string) => readFileSync(path, 'utf-8'),
|
||||||
|
cat: (path: string) => { }, // added below
|
||||||
|
'read-bytes': (path: string) => [...readFileSync(path)],
|
||||||
|
|
||||||
|
// Writing
|
||||||
|
write: (path: string, content: string) => writeFileSync(path, content),
|
||||||
|
append: (path: string, content: string) => appendFileSync(path, content),
|
||||||
|
|
||||||
|
// File operations
|
||||||
|
delete: (path: string) => rmSync(path),
|
||||||
|
rm: (path: string) => { }, // added below
|
||||||
|
copy: (from: string, to: string) => copyFileSync(from, to),
|
||||||
|
move: (from: string, to: string) => {
|
||||||
|
fs.copy(from, to)
|
||||||
|
fs.rm(from)
|
||||||
|
},
|
||||||
|
mv: (from: string, to: string) => { }, // added below
|
||||||
|
|
||||||
|
// Path operations
|
||||||
|
basename: (path: string) => basename(path),
|
||||||
|
dirname: (path: string) => dirname(path),
|
||||||
|
extname: (path: string) => extname(path),
|
||||||
|
join: (...paths: string[]) => join(...paths),
|
||||||
|
resolve: (...paths: string[]) => resolve(...paths),
|
||||||
|
|
||||||
|
// File info
|
||||||
|
stat: (path: string) => {
|
||||||
|
try {
|
||||||
|
const stats = statSync(path)
|
||||||
|
const record = Object.fromEntries(Object.entries(stats))
|
||||||
|
record['atime'] = record['atimeMs']
|
||||||
|
record['ctime'] = record['ctimeMs']
|
||||||
|
record['mtime'] = record['mtimeMs']
|
||||||
|
|
||||||
|
delete record['atimeMs']
|
||||||
|
delete record['ctimeMs']
|
||||||
|
delete record['mtimeMs']
|
||||||
|
|
||||||
|
return record
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
'exists?': (path: string) => {
|
||||||
|
try {
|
||||||
|
statSync(path)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'file?': (path: string) => {
|
||||||
|
try { return statSync(path).isFile() }
|
||||||
|
catch { return false }
|
||||||
|
},
|
||||||
|
'dir?': (path: string) => {
|
||||||
|
try { return statSync(path).isDirectory() }
|
||||||
|
catch { return false }
|
||||||
|
},
|
||||||
|
'symlink?': (path: string) => {
|
||||||
|
try { return lstatSync(path).isSymbolicLink() }
|
||||||
|
catch { return false }
|
||||||
|
},
|
||||||
|
'exec?': (path: string) => {
|
||||||
|
try {
|
||||||
|
const stats = statSync(path)
|
||||||
|
return !!(stats.mode & 0o111)
|
||||||
|
}
|
||||||
|
catch { return false }
|
||||||
|
},
|
||||||
|
size: (path: string) => {
|
||||||
|
try { return statSync(path).size }
|
||||||
|
catch { return 0 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
chmod: (path: string, mode: number | string) => {
|
||||||
|
const numMode = typeof mode === 'string' ? parseInt(mode, 8) : mode
|
||||||
|
chmodSync(path, numMode)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Symlinks
|
||||||
|
symlink: (target: string, path: string) => symlinkSync(target, path),
|
||||||
|
readlink: (path: string) => readlinkSync(path, 'utf-8'),
|
||||||
|
|
||||||
|
// Other
|
||||||
|
glob: (pattern: string) => {
|
||||||
|
const dir = pattern.substring(0, pattern.lastIndexOf('/'))
|
||||||
|
const match = pattern.substring(pattern.lastIndexOf('/') + 1)
|
||||||
|
|
||||||
|
if (!match.includes('*')) throw new Error('only * patterns supported')
|
||||||
|
|
||||||
|
const ext = match.split('*').pop()!
|
||||||
|
return readdirSync(dir)
|
||||||
|
.filter((f) => f.endsWith(ext))
|
||||||
|
.map((f) => join(dir, f))
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: (path: string, callback: Function) =>
|
||||||
|
watch(path, (event, filename) => callback(event, filename)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
; (fs as any).cat = fs.read
|
||||||
|
; (fs as any).mv = fs.move
|
||||||
|
; (fs as any).cp = fs.copy
|
||||||
|
; (fs as any).rm = fs.delete
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from 'reefvm'
|
} from 'reefvm'
|
||||||
|
|
||||||
import { dict } from './dict'
|
import { dict } from './dict'
|
||||||
|
import { fs } from './fs'
|
||||||
import { json } from './json'
|
import { json } from './json'
|
||||||
import { load } from './load'
|
import { load } from './load'
|
||||||
import { list } from './list'
|
import { list } from './list'
|
||||||
|
|
@ -14,6 +15,7 @@ import { str } from './str'
|
||||||
|
|
||||||
export const globals = {
|
export const globals = {
|
||||||
dict,
|
dict,
|
||||||
|
fs,
|
||||||
json,
|
json,
|
||||||
load,
|
load,
|
||||||
list,
|
list,
|
||||||
|
|
@ -43,6 +45,25 @@ export const globals = {
|
||||||
return typeof v !== 'string' || this.scope.has(v)
|
return typeof v !== 'string' || this.scope.has(v)
|
||||||
},
|
},
|
||||||
ref: (fn: Function) => fn,
|
ref: (fn: Function) => fn,
|
||||||
|
import: function (this: VM, atNamed: Record<any, string | string[]> = {}, ...idents: string[]) {
|
||||||
|
const onlyArray = Array.isArray(atNamed.only) ? atNamed.only : [atNamed.only].filter(a => a)
|
||||||
|
const only = new Set(onlyArray)
|
||||||
|
const wantsOnly = only.size > 0
|
||||||
|
|
||||||
|
|
||||||
|
for (const ident of idents) {
|
||||||
|
const module = this.get(ident)
|
||||||
|
|
||||||
|
if (!module) throw new Error(`import: can't find ${ident}`)
|
||||||
|
if (module.type !== 'dict') throw new Error(`import: can't import ${module.type}`)
|
||||||
|
|
||||||
|
for (const [name, value] of module.value.entries()) {
|
||||||
|
if (value.type === 'dict') throw new Error(`import: can't import dicts in dicts`)
|
||||||
|
if (wantsOnly && !only.has(name)) continue
|
||||||
|
this.set(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// env
|
// env
|
||||||
args: Bun.argv.slice(1),
|
args: Bun.argv.slice(1),
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ export const load = async function (this: VM, path: string): Promise<Record<stri
|
||||||
const scope = this.scope
|
const scope = this.scope
|
||||||
const pc = this.pc
|
const pc = this.pc
|
||||||
|
|
||||||
const fullPath = resolve(path) + '.sh'
|
let fullPath = resolve(path)
|
||||||
|
if (!path.includes('.')) fullPath += '.sh'
|
||||||
|
|
||||||
const code = readFileSync(fullPath, 'utf-8')
|
const code = readFileSync(fullPath, 'utf-8')
|
||||||
|
|
||||||
this.pc = this.instructions.length
|
this.pc = this.instructions.length
|
||||||
|
|
|
||||||
329
src/prelude/tests/fs.test.ts
Normal file
329
src/prelude/tests/fs.test.ts
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
import { expect, describe, test, beforeEach, afterEach } from 'bun:test'
|
||||||
|
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs'
|
||||||
|
import { join, resolve } from 'path'
|
||||||
|
import { fs } from '../fs'
|
||||||
|
|
||||||
|
const TEST_DIR = resolve('./tmp/shrimp-fs-test')
|
||||||
|
const CWD = process.cwd()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (existsSync(TEST_DIR)) {
|
||||||
|
rmSync(TEST_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
mkdirSync(TEST_DIR, { recursive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.chdir(CWD)
|
||||||
|
if (existsSync(TEST_DIR)) {
|
||||||
|
rmSync(TEST_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fs - directory operations', () => {
|
||||||
|
test('fs.ls lists directory contents', () => {
|
||||||
|
writeFileSync(join(TEST_DIR, 'file1.txt'), 'content1')
|
||||||
|
writeFileSync(join(TEST_DIR, 'file2.txt'), 'content2')
|
||||||
|
|
||||||
|
const result = fs.ls(TEST_DIR)
|
||||||
|
expect(result).toContain('file1.txt')
|
||||||
|
expect(result).toContain('file2.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.mkdir creates directory', () => {
|
||||||
|
const newDir = join(TEST_DIR, 'newdir')
|
||||||
|
fs.mkdir(newDir)
|
||||||
|
expect(existsSync(newDir)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.rmdir removes empty directory', () => {
|
||||||
|
const dir = join(TEST_DIR, 'toremove')
|
||||||
|
mkdirSync(dir)
|
||||||
|
fs.rmdir(dir)
|
||||||
|
expect(existsSync(dir)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.pwd returns current working directory', () => {
|
||||||
|
const result = fs.pwd()
|
||||||
|
expect(typeof result).toBe('string')
|
||||||
|
expect(result.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.cd changes current working directory', () => {
|
||||||
|
const originalCwd = process.cwd()
|
||||||
|
fs.cd(TEST_DIR)
|
||||||
|
expect(process.cwd()).toBe(TEST_DIR)
|
||||||
|
process.chdir(originalCwd) // restore
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fs - reading', () => {
|
||||||
|
test('fs.read reads file contents as string', () => {
|
||||||
|
const file = join(TEST_DIR, 'test.txt')
|
||||||
|
writeFileSync(file, 'hello world')
|
||||||
|
|
||||||
|
const result = fs.read(file)
|
||||||
|
expect(result).toBe('hello world')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.cat is alias for fs.read', () => {
|
||||||
|
const file = join(TEST_DIR, 'test.txt')
|
||||||
|
writeFileSync(file, 'hello world')
|
||||||
|
|
||||||
|
const result = fs.cat(file)
|
||||||
|
expect(result).toBe('hello world')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.read-bytes reads file as buffer', () => {
|
||||||
|
const file = join(TEST_DIR, 'test.bin')
|
||||||
|
writeFileSync(file, Buffer.from([1, 2, 3, 4]))
|
||||||
|
|
||||||
|
const result = fs['read-bytes'](file)
|
||||||
|
expect(result).toBeInstanceOf(Array)
|
||||||
|
expect(result).toEqual([1, 2, 3, 4])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fs - writing', () => {
|
||||||
|
test('fs.write writes string to file', async () => {
|
||||||
|
const file = join(TEST_DIR, 'output.txt')
|
||||||
|
fs.write(file, 'test content')
|
||||||
|
|
||||||
|
const content = Bun.file(file).text()
|
||||||
|
expect(await content).toBe('test content')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.append appends to existing file', async () => {
|
||||||
|
const file = join(TEST_DIR, 'append.txt')
|
||||||
|
writeFileSync(file, 'first')
|
||||||
|
fs.append(file, ' second')
|
||||||
|
|
||||||
|
const content = await Bun.file(file).text()
|
||||||
|
expect(content).toBe('first second')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fs - file operations', () => {
|
||||||
|
test('fs.rm removes file', () => {
|
||||||
|
const file = join(TEST_DIR, 'remove.txt')
|
||||||
|
writeFileSync(file, 'content')
|
||||||
|
|
||||||
|
fs.rm(file)
|
||||||
|
expect(existsSync(file)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.delete is alias for fs.rm', () => {
|
||||||
|
const file = join(TEST_DIR, 'delete.txt')
|
||||||
|
writeFileSync(file, 'content')
|
||||||
|
|
||||||
|
fs.delete(file)
|
||||||
|
expect(existsSync(file)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.copy copies file', async () => {
|
||||||
|
const src = join(TEST_DIR, 'source.txt')
|
||||||
|
const dest = join(TEST_DIR, 'dest.txt')
|
||||||
|
writeFileSync(src, 'content')
|
||||||
|
|
||||||
|
fs.copy(src, dest)
|
||||||
|
expect(await Bun.file(dest).text()).toBe('content')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.cp is alias for fs.copy', async () => {
|
||||||
|
const src = join(TEST_DIR, 'source2.txt')
|
||||||
|
const dest = join(TEST_DIR, 'dest2.txt')
|
||||||
|
writeFileSync(src, 'content')
|
||||||
|
|
||||||
|
fs.cp(src, dest)
|
||||||
|
expect(await Bun.file(dest).text()).toBe('content')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.move moves file', async () => {
|
||||||
|
const src = join(TEST_DIR, 'source.txt')
|
||||||
|
const dest = join(TEST_DIR, 'moved.txt')
|
||||||
|
writeFileSync(src, 'content')
|
||||||
|
|
||||||
|
fs.move(src, dest)
|
||||||
|
expect(existsSync(src)).toBe(false)
|
||||||
|
expect(await Bun.file(dest).text()).toBe('content')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.mv is alias for fs.move', async () => {
|
||||||
|
const src = join(TEST_DIR, 'source2.txt')
|
||||||
|
const dest = join(TEST_DIR, 'moved2.txt')
|
||||||
|
writeFileSync(src, 'content')
|
||||||
|
|
||||||
|
fs.mv(src, dest)
|
||||||
|
expect(existsSync(src)).toBe(false)
|
||||||
|
expect(await Bun.file(dest).text()).toBe('content')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fs - path operations', () => {
|
||||||
|
test('fs.basename extracts filename from path', () => {
|
||||||
|
expect(fs.basename('/path/to/file.txt')).toBe('file.txt')
|
||||||
|
expect(fs.basename('/path/to/dir/')).toBe('dir')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.dirname extracts directory from path', () => {
|
||||||
|
expect(fs.dirname('/path/to/file.txt')).toBe('/path/to')
|
||||||
|
expect(fs.dirname('/path/to/dir/')).toBe('/path/to')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.extname extracts file extension', () => {
|
||||||
|
expect(fs.extname('file.txt')).toBe('.txt')
|
||||||
|
expect(fs.extname('file.tar.gz')).toBe('.gz')
|
||||||
|
expect(fs.extname('noext')).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.join joins path segments', () => {
|
||||||
|
expect(fs.join('path', 'to', 'file.txt')).toBe('path/to/file.txt')
|
||||||
|
expect(fs.join('/absolute', 'path')).toBe('/absolute/path')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.resolve resolves to absolute path', () => {
|
||||||
|
const result = fs.resolve('relative', 'path')
|
||||||
|
expect(result.startsWith('/')).toBe(true)
|
||||||
|
expect(result).toContain('relative')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fs - file info', () => {
|
||||||
|
test('fs.stat returns file stats', () => {
|
||||||
|
const file = join(TEST_DIR, 'stat.txt')
|
||||||
|
writeFileSync(file, 'content')
|
||||||
|
|
||||||
|
const stats = fs.stat(file)
|
||||||
|
expect(stats).toHaveProperty('size')
|
||||||
|
expect(stats).toHaveProperty('mtime')
|
||||||
|
expect(stats.size).toBe(7) // 'content' is 7 bytes
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.exists? checks if path exists', () => {
|
||||||
|
const file = join(TEST_DIR, 'exists.txt')
|
||||||
|
expect(fs['exists?'](file)).toBe(false)
|
||||||
|
|
||||||
|
writeFileSync(file, 'content')
|
||||||
|
expect(fs['exists?'](file)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.file? checks if path is a file', () => {
|
||||||
|
const file = join(TEST_DIR, 'isfile.txt')
|
||||||
|
writeFileSync(file, 'content')
|
||||||
|
|
||||||
|
expect(fs['file?'](file)).toBe(true)
|
||||||
|
expect(fs['file?'](TEST_DIR)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.dir? checks if path is a directory', () => {
|
||||||
|
const dir = join(TEST_DIR, 'isdir')
|
||||||
|
mkdirSync(dir)
|
||||||
|
|
||||||
|
expect(fs['dir?'](dir)).toBe(true)
|
||||||
|
expect(fs['dir?'](join(TEST_DIR, 'isfile.txt'))).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.symlink? checks if path is a symbolic link', () => {
|
||||||
|
const file = join(TEST_DIR, 'target.txt')
|
||||||
|
const link = join(TEST_DIR, 'link.txt')
|
||||||
|
writeFileSync(file, 'content')
|
||||||
|
|
||||||
|
fs.symlink(file, link)
|
||||||
|
expect(fs['symlink?'](link)).toBe(true)
|
||||||
|
expect(fs['symlink?'](file)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.exec? checks if file is executable', () => {
|
||||||
|
const file = join(TEST_DIR, 'script.sh')
|
||||||
|
writeFileSync(file, '#!/bin/bash\necho hello')
|
||||||
|
|
||||||
|
fs.chmod(file, 0o755)
|
||||||
|
expect(fs['exec?'](file)).toBe(true)
|
||||||
|
|
||||||
|
fs.chmod(file, 0o644)
|
||||||
|
expect(fs['exec?'](file)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.size returns file size in bytes', () => {
|
||||||
|
const file = join(TEST_DIR, 'sizeme.txt')
|
||||||
|
writeFileSync(file, 'content')
|
||||||
|
|
||||||
|
expect(fs.size(file)).toBe(7) // 'content' is 7 bytes
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fs - permissions', () => {
|
||||||
|
test('fs.chmod changes file permissions with octal number', () => {
|
||||||
|
const file = join(TEST_DIR, 'perms.txt')
|
||||||
|
writeFileSync(file, 'content')
|
||||||
|
|
||||||
|
fs.chmod(file, 0o755)
|
||||||
|
expect(fs['exec?'](file)).toBe(true)
|
||||||
|
|
||||||
|
fs.chmod(file, 0o644)
|
||||||
|
expect(fs['exec?'](file)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.chmod changes file permissions with string', () => {
|
||||||
|
const file = join(TEST_DIR, 'perms2.txt')
|
||||||
|
writeFileSync(file, 'content')
|
||||||
|
|
||||||
|
fs.chmod(file, '755')
|
||||||
|
expect(fs['exec?'](file)).toBe(true)
|
||||||
|
|
||||||
|
fs.chmod(file, '644')
|
||||||
|
expect(fs['exec?'](file)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fs - symlinks', () => {
|
||||||
|
test('fs.symlink creates symbolic link', () => {
|
||||||
|
const target = join(TEST_DIR, 'target.txt')
|
||||||
|
const link = join(TEST_DIR, 'link.txt')
|
||||||
|
writeFileSync(target, 'content')
|
||||||
|
|
||||||
|
fs.symlink(target, link)
|
||||||
|
expect(fs['symlink?'](link)).toBe(true)
|
||||||
|
expect(fs.read(link)).toBe('content')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.readlink reads symbolic link target', () => {
|
||||||
|
const target = join(TEST_DIR, 'target.txt')
|
||||||
|
const link = join(TEST_DIR, 'link.txt')
|
||||||
|
writeFileSync(target, 'content')
|
||||||
|
|
||||||
|
fs.symlink(target, link)
|
||||||
|
expect(fs.readlink(link)).toBe(target)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fs - other', () => {
|
||||||
|
test('fs.glob matches file patterns', () => {
|
||||||
|
writeFileSync(join(TEST_DIR, 'file1.txt'), '')
|
||||||
|
writeFileSync(join(TEST_DIR, 'file2.txt'), '')
|
||||||
|
writeFileSync(join(TEST_DIR, 'file3.md'), '')
|
||||||
|
|
||||||
|
const result = fs.glob(join(TEST_DIR, '*.txt'))
|
||||||
|
expect(result).toHaveLength(2)
|
||||||
|
expect(result).toContain(join(TEST_DIR, 'file1.txt'))
|
||||||
|
expect(result).toContain(join(TEST_DIR, 'file2.txt'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fs.watch calls callback on file change', async () => {
|
||||||
|
const file = join(TEST_DIR, 'watch.txt')
|
||||||
|
writeFileSync(file, 'initial')
|
||||||
|
|
||||||
|
let called = false
|
||||||
|
const watcher = fs.watch(file, () => { called = true })
|
||||||
|
|
||||||
|
// Trigger change
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
writeFileSync(file, 'updated')
|
||||||
|
|
||||||
|
// Wait for watcher
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
expect(called).toBe(true)
|
||||||
|
watcher.close?.()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,41 +1,41 @@
|
||||||
import { expect, describe, test } from 'bun:test'
|
import { expect, describe, test } from 'bun:test'
|
||||||
import { globals } from '#prelude'
|
import { globals } from '#prelude'
|
||||||
|
|
||||||
describe('use', () => {
|
describe('loading a file', () => {
|
||||||
test(`imports all a file's functions`, async () => {
|
test(`imports all a file's functions`, async () => {
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math
|
math = load ./src/prelude/tests/math.sh
|
||||||
math.double 4
|
math.double 4
|
||||||
`).toEvaluateTo(8, globals)
|
`).toEvaluateTo(8, globals)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math
|
math = load ./src/prelude/tests/math.sh
|
||||||
math.double (math.double 4)
|
math.double (math.double 4)
|
||||||
`).toEvaluateTo(16, globals)
|
`).toEvaluateTo(16, globals)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math
|
math = load ./src/prelude/tests/math.sh
|
||||||
dbl = math.double
|
dbl = ref math.double
|
||||||
dbl (dbl 2)
|
dbl (dbl 2)
|
||||||
`).toEvaluateTo(8, globals)
|
`).toEvaluateTo(8, globals)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math
|
math = load ./src/prelude/tests/math.sh
|
||||||
math.pi
|
math.pi
|
||||||
`).toEvaluateTo(3.14, globals)
|
`).toEvaluateTo(3.14, globals)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math
|
math = load ./src/prelude/tests/math.sh
|
||||||
math | at 🥧
|
math | at 🥧
|
||||||
`).toEvaluateTo(3.14159265359, globals)
|
`).toEvaluateTo(3.14159265359, globals)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math
|
math = load ./src/prelude/tests/math.sh
|
||||||
math.🥧
|
math.🥧
|
||||||
`).toEvaluateTo(3.14159265359, globals)
|
`).toEvaluateTo(3.14159265359, globals)
|
||||||
|
|
||||||
expect(`
|
expect(`
|
||||||
math = load ./src/prelude/tests/math
|
math = load ./src/prelude/tests/math.sh
|
||||||
math.add1 5
|
math.add1 5
|
||||||
`).toEvaluateTo(6, globals)
|
`).toEvaluateTo(6, globals)
|
||||||
})
|
})
|
||||||
Loading…
Reference in New Issue
Block a user