Compare commits

...

9 Commits

15 changed files with 701 additions and 42 deletions

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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'])
})
})

View File

@ -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)
}) })
}) })

View File

@ -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 |

View File

@ -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

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: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
}) })

View File

@ -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?
`)
})
})

View File

@ -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
`) `)
}) })

View File

@ -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
View 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

View File

@ -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),

View File

@ -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

View 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?.()
})
})

View File

@ -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)
}) })