Compare commits
7 Commits
69bbe17992
...
c273429b24
| Author | SHA1 | Date | |
|---|---|---|---|
| c273429b24 | |||
| c127566abe | |||
| b7a65e07dc | |||
| 8299022b4f | |||
| 131c943fc6 | |||
| 866da86862 | |||
| 5ac0b02044 |
4
bin/repl
4
bin/repl
|
|
@ -145,7 +145,7 @@ async function repl() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const compiler = new Compiler(trimmed, [...Object.keys(globals), ...vm.vars()])
|
const compiler = new Compiler(trimmed, Object.keys(globals))
|
||||||
|
|
||||||
// Save VM state before appending bytecode, in case execution fails
|
// Save VM state before appending bytecode, in case execution fails
|
||||||
const savedInstructions = [...vm.instructions]
|
const savedInstructions = [...vm.instructions]
|
||||||
|
|
@ -235,7 +235,7 @@ async function loadFile(filePath: string): Promise<{ vm: VM; codeHistory: string
|
||||||
if (!trimmed) continue
|
if (!trimmed) continue
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const compiler = new Compiler(trimmed, [...Object.keys(globals), ...vm.vars()])
|
const compiler = new Compiler(trimmed)
|
||||||
vm.appendBytecode(compiler.bytecode)
|
vm.appendBytecode(compiler.bytecode)
|
||||||
await vm.continue()
|
await vm.continue()
|
||||||
codeHistory.push(trimmed)
|
codeHistory.push(trimmed)
|
||||||
|
|
|
||||||
47
bin/shrimp
47
bin/shrimp
|
|
@ -1,14 +1,50 @@
|
||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { colors } from '../src/prelude'
|
import { Compiler } from '../src/compiler/compiler'
|
||||||
|
import { colors, globals } from '../src/prelude'
|
||||||
|
import { parser } from '../src/parser/shrimp'
|
||||||
import { treeToString } from '../src/utils/tree'
|
import { treeToString } from '../src/utils/tree'
|
||||||
import { runFile, compileFile, parseCode } from '../src'
|
import { VM, fromValue, bytecodeToString } from 'reefvm'
|
||||||
import { bytecodeToString } from 'reefvm'
|
|
||||||
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
|
async function runFile(filePath: string) {
|
||||||
|
try {
|
||||||
|
const code = readFileSync(filePath, 'utf-8')
|
||||||
|
const compiler = new Compiler(code, Object.keys(globals))
|
||||||
|
const vm = new VM(compiler.bytecode, globals)
|
||||||
|
await vm.run()
|
||||||
|
return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]) : null
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`${colors.red}Error:${colors.reset} ${error.message}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compileFile(filePath: string) {
|
||||||
|
try {
|
||||||
|
const code = readFileSync(filePath, 'utf-8')
|
||||||
|
const compiler = new Compiler(code)
|
||||||
|
return bytecodeToString(compiler.bytecode)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`${colors.red}Error:${colors.reset} ${error.message}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseFile(filePath: string) {
|
||||||
|
try {
|
||||||
|
const code = readFileSync(filePath, 'utf-8')
|
||||||
|
const tree = parser.parse(code)
|
||||||
|
return treeToString(tree, code)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`${colors.red}Error:${colors.reset} ${error.message}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showHelp() {
|
function showHelp() {
|
||||||
console.log(`${colors.bright}${colors.magenta}🦐 Shrimp${colors.reset} is a scripting language in a shell.
|
console.log(`${colors.bright}${colors.magenta}🦐 Shrimp${colors.reset} is a scripting language in a shell.
|
||||||
|
|
||||||
|
|
@ -76,7 +112,7 @@ async function main() {
|
||||||
console.log(`${colors.bright}usage: shrimp bytecode <file>${colors.reset}`)
|
console.log(`${colors.bright}usage: shrimp bytecode <file>${colors.reset}`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
console.log(bytecodeToString(compileFile(file)))
|
console.log(await compileFile(file))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,8 +122,7 @@ async function main() {
|
||||||
console.log(`${colors.bright}usage: shrimp parse <file>${colors.reset}`)
|
console.log(`${colors.bright}usage: shrimp parse <file>${colors.reset}`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
const input = readFileSync(file, 'utf-8')
|
console.log(await parseFile(file))
|
||||||
console.log(treeToString(parseCode(input), input))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
8
bun.lock
8
bun.lock
|
|
@ -44,7 +44,7 @@
|
||||||
|
|
||||||
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
|
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
|
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
|
|
||||||
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.15", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-qtAXMNGG4R0UGGI8zWrqm2B7BdXqx48vunJXBPzfDOHPA5WkRUZdTSbE7TFwO4jLhYqSE23YMWsM9NhE6ovobw=="],
|
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.15", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-qtAXMNGG4R0UGGI8zWrqm2B7BdXqx48vunJXBPzfDOHPA5WkRUZdTSbE7TFwO4jLhYqSE23YMWsM9NhE6ovobw=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
|
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
||||||
|
|
||||||
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
|
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
|
||||||
|
|
||||||
|
|
@ -62,11 +62,11 @@
|
||||||
|
|
||||||
"hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="],
|
"hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="],
|
||||||
|
|
||||||
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#3e2e68b31f504347225a4d705c7568a0957d629e", { "peerDependencies": { "typescript": "^5" } }, "3e2e68b31f504347225a4d705c7568a0957d629e"],
|
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#bffb83a5280a4d74e424c4e0f4fbd46f790227a3", { "peerDependencies": { "typescript": "^5" } }, "bffb83a5280a4d74e424c4e0f4fbd46f790227a3"],
|
||||||
|
|
||||||
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
|
"tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"dev": "bun generate-parser && bun --hot src/server/server.tsx",
|
"dev": "bun generate-parser && bun --hot src/server/server.tsx",
|
||||||
"generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts",
|
"generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts",
|
||||||
"repl": "bun generate-parser && bun bin/repl",
|
"repl": "bun generate-parser && bun bin/repl",
|
||||||
"update-reef": "rm -rf ~/.bun/install/cache/ && rm bun.lock && bun update reefvm",
|
"update-reef": "rm -rf ~/.bun/install/cache/ && rm bun.lock && bun update reefvm"
|
||||||
"cli:install": "ln -s \"$(pwd)/bin/shrimp\" ~/.bun/bin/shrimp",
|
|
||||||
"cli:remove": "rm ~/.bun/bin/shrimp"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/view": "^6.38.3",
|
"@codemirror/view": "^6.38.3",
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ function processEscapeSeq(escapeSeq: string): string {
|
||||||
|
|
||||||
export class Compiler {
|
export class Compiler {
|
||||||
instructions: ProgramItem[] = []
|
instructions: ProgramItem[] = []
|
||||||
labelCount = 0
|
|
||||||
fnLabelCount = 0
|
fnLabelCount = 0
|
||||||
ifLabelCount = 0
|
ifLabelCount = 0
|
||||||
tryLabelCount = 0
|
tryLabelCount = 0
|
||||||
|
|
@ -60,9 +59,9 @@ export class Compiler {
|
||||||
bytecode: Bytecode
|
bytecode: Bytecode
|
||||||
pipeCounter = 0
|
pipeCounter = 0
|
||||||
|
|
||||||
constructor(public input: string, globals?: string[] | Record<string, any>) {
|
constructor(public input: string, globals?: string[]) {
|
||||||
try {
|
try {
|
||||||
if (globals) setGlobals(Array.isArray(globals) ? globals : Object.keys(globals))
|
if (globals) setGlobals(globals)
|
||||||
const cst = parser.parse(input)
|
const cst = parser.parse(input)
|
||||||
const errors = checkTreeForErrors(cst)
|
const errors = checkTreeForErrors(cst)
|
||||||
|
|
||||||
|
|
@ -107,21 +106,11 @@ 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
|
const number = Number(value)
|
||||||
// Number() doesn't parse '-0xFF' or '+0xFF' correctly
|
if (Number.isNaN(number))
|
||||||
let numberValue: number
|
|
||||||
if (value.startsWith('-') && (value.includes('0x') || value.includes('0b'))) {
|
|
||||||
numberValue = -Number(value.slice(1))
|
|
||||||
} else if (value.startsWith('+') && (value.includes('0x') || value.includes('0b'))) {
|
|
||||||
numberValue = Number(value.slice(1))
|
|
||||||
} else {
|
|
||||||
numberValue = Number(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Number.isNaN(numberValue))
|
|
||||||
throw new CompilerError(`Invalid number literal: ${value}`, node.from, node.to)
|
throw new CompilerError(`Invalid number literal: ${value}`, node.from, node.to)
|
||||||
|
|
||||||
return [[`PUSH`, numberValue]]
|
return [[`PUSH`, number]]
|
||||||
|
|
||||||
case terms.String: {
|
case terms.String: {
|
||||||
if (node.firstChild?.type.id === terms.CurlyString)
|
if (node.firstChild?.type.id === terms.CurlyString)
|
||||||
|
|
@ -247,24 +236,6 @@ export class Compiler {
|
||||||
case '%':
|
case '%':
|
||||||
instructions.push(['MOD'])
|
instructions.push(['MOD'])
|
||||||
break
|
break
|
||||||
case 'band':
|
|
||||||
instructions.push(['BIT_AND'])
|
|
||||||
break
|
|
||||||
case 'bor':
|
|
||||||
instructions.push(['BIT_OR'])
|
|
||||||
break
|
|
||||||
case 'bxor':
|
|
||||||
instructions.push(['BIT_XOR'])
|
|
||||||
break
|
|
||||||
case '<<':
|
|
||||||
instructions.push(['BIT_SHL'])
|
|
||||||
break
|
|
||||||
case '>>':
|
|
||||||
instructions.push(['BIT_SHR'])
|
|
||||||
break
|
|
||||||
case '>>>':
|
|
||||||
instructions.push(['BIT_USHR'])
|
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
throw new CompilerError(`Unsupported binary operator: ${opValue}`, op.from, op.to)
|
throw new CompilerError(`Unsupported binary operator: ${opValue}`, op.from, op.to)
|
||||||
}
|
}
|
||||||
|
|
@ -307,31 +278,13 @@ export class Compiler {
|
||||||
const { identifier, operator, right } = getCompoundAssignmentParts(node)
|
const { identifier, operator, right } = getCompoundAssignmentParts(node)
|
||||||
const identifierName = input.slice(identifier.from, identifier.to)
|
const identifierName = input.slice(identifier.from, identifier.to)
|
||||||
const instructions: ProgramItem[] = []
|
const instructions: ProgramItem[] = []
|
||||||
const opValue = input.slice(operator.from, operator.to)
|
|
||||||
|
|
||||||
// Special handling for ??= since it needs conditional evaluation
|
// will throw if undefined
|
||||||
if (opValue === '??=') {
|
|
||||||
instructions.push(['LOAD', identifierName])
|
instructions.push(['LOAD', identifierName])
|
||||||
|
|
||||||
const rightInstructions = this.#compileNode(right, input)
|
|
||||||
|
|
||||||
instructions.push(['DUP'])
|
|
||||||
instructions.push(['PUSH', null])
|
|
||||||
instructions.push(['NEQ'])
|
|
||||||
instructions.push(['JUMP_IF_TRUE', rightInstructions.length + 1])
|
|
||||||
instructions.push(['POP'])
|
|
||||||
instructions.push(...rightInstructions)
|
|
||||||
|
|
||||||
instructions.push(['DUP'])
|
|
||||||
instructions.push(['STORE', identifierName])
|
|
||||||
|
|
||||||
return instructions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard compound assignments: evaluate both sides, then operate
|
|
||||||
instructions.push(['LOAD', identifierName]) // will throw if undefined
|
|
||||||
instructions.push(...this.#compileNode(right, input))
|
instructions.push(...this.#compileNode(right, input))
|
||||||
|
|
||||||
|
const opValue = input.slice(operator.from, operator.to)
|
||||||
switch (opValue) {
|
switch (opValue) {
|
||||||
case '+=':
|
case '+=':
|
||||||
instructions.push(['ADD'])
|
instructions.push(['ADD'])
|
||||||
|
|
@ -418,29 +371,7 @@ export class Compiler {
|
||||||
|
|
||||||
case terms.FunctionCallOrIdentifier: {
|
case terms.FunctionCallOrIdentifier: {
|
||||||
if (node.firstChild?.type.id === terms.DotGet) {
|
if (node.firstChild?.type.id === terms.DotGet) {
|
||||||
const instructions: ProgramItem[] = []
|
return this.#compileNode(node.firstChild, input)
|
||||||
const callLabel = `.call_dotget_${++this.labelCount}`
|
|
||||||
const afterLabel = `.after_dotget_${++this.labelCount}`
|
|
||||||
|
|
||||||
instructions.push(...this.#compileNode(node.firstChild, input))
|
|
||||||
instructions.push(['DUP'])
|
|
||||||
instructions.push(['TYPE'])
|
|
||||||
instructions.push(['PUSH', 'function'])
|
|
||||||
instructions.push(['EQ'])
|
|
||||||
instructions.push(['JUMP_IF_TRUE', callLabel])
|
|
||||||
instructions.push(['DUP'])
|
|
||||||
instructions.push(['TYPE'])
|
|
||||||
instructions.push(['PUSH', 'native'])
|
|
||||||
instructions.push(['EQ'])
|
|
||||||
instructions.push(['JUMP_IF_TRUE', callLabel])
|
|
||||||
instructions.push(['JUMP', afterLabel])
|
|
||||||
instructions.push([`${callLabel}:`])
|
|
||||||
instructions.push(['PUSH', 0])
|
|
||||||
instructions.push(['PUSH', 0])
|
|
||||||
instructions.push(['CALL'])
|
|
||||||
instructions.push([`${afterLabel}:`])
|
|
||||||
|
|
||||||
return instructions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [['TRY_CALL', value]]
|
return [['TRY_CALL', value]]
|
||||||
|
|
@ -660,18 +591,6 @@ export class Compiler {
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case '??':
|
|
||||||
// Nullish coalescing: return left if not null, else right
|
|
||||||
instructions.push(...leftInstructions)
|
|
||||||
instructions.push(['DUP'])
|
|
||||||
instructions.push(['PUSH', null])
|
|
||||||
instructions.push(['NEQ'])
|
|
||||||
instructions.push(['JUMP_IF_TRUE', rightInstructions.length + 1])
|
|
||||||
instructions.push(['POP'])
|
|
||||||
instructions.push(...rightInstructions)
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new CompilerError(`Unsupported conditional operator: ${opValue}`, op.from, op.to)
|
throw new CompilerError(`Unsupported conditional operator: ${opValue}`, op.from, op.to)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
import { expect, describe, test } from 'bun:test'
|
|
||||||
|
|
||||||
describe('bitwise operators', () => {
|
|
||||||
describe('band (bitwise AND)', () => {
|
|
||||||
test('basic AND operation', () => {
|
|
||||||
expect('5 band 3').toEvaluateTo(1)
|
|
||||||
// 5 = 0101, 3 = 0011, result = 0001 = 1
|
|
||||||
})
|
|
||||||
|
|
||||||
test('AND with zero', () => {
|
|
||||||
expect('5 band 0').toEvaluateTo(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('AND with all bits set', () => {
|
|
||||||
expect('15 band 7').toEvaluateTo(7)
|
|
||||||
// 15 = 1111, 7 = 0111, result = 0111 = 7
|
|
||||||
})
|
|
||||||
|
|
||||||
test('AND in assignment', () => {
|
|
||||||
expect('x = 12 band 10').toEvaluateTo(8)
|
|
||||||
// 12 = 1100, 10 = 1010, result = 1000 = 8
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('bor (bitwise OR)', () => {
|
|
||||||
test('basic OR operation', () => {
|
|
||||||
expect('5 bor 3').toEvaluateTo(7)
|
|
||||||
// 5 = 0101, 3 = 0011, result = 0111 = 7
|
|
||||||
})
|
|
||||||
|
|
||||||
test('OR with zero', () => {
|
|
||||||
expect('5 bor 0').toEvaluateTo(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('OR with all bits set', () => {
|
|
||||||
expect('8 bor 4').toEvaluateTo(12)
|
|
||||||
// 8 = 1000, 4 = 0100, result = 1100 = 12
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('bxor (bitwise XOR)', () => {
|
|
||||||
test('basic XOR operation', () => {
|
|
||||||
expect('5 bxor 3').toEvaluateTo(6)
|
|
||||||
// 5 = 0101, 3 = 0011, result = 0110 = 6
|
|
||||||
})
|
|
||||||
|
|
||||||
test('XOR with itself returns zero', () => {
|
|
||||||
expect('5 bxor 5').toEvaluateTo(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('XOR with zero returns same value', () => {
|
|
||||||
expect('7 bxor 0').toEvaluateTo(7)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('XOR in assignment', () => {
|
|
||||||
expect('result = 8 bxor 12').toEvaluateTo(4)
|
|
||||||
// 8 = 1000, 12 = 1100, result = 0100 = 4
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('bnot (bitwise NOT)', () => {
|
|
||||||
test('NOT of positive number', () => {
|
|
||||||
expect('bnot 5').toEvaluateTo(-6)
|
|
||||||
// ~5 = -6 (two\'s complement)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('NOT of zero', () => {
|
|
||||||
expect('bnot 0').toEvaluateTo(-1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('NOT of negative number', () => {
|
|
||||||
expect('bnot -1').toEvaluateTo(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('double NOT returns original', () => {
|
|
||||||
expect('bnot (bnot 5)').toEvaluateTo(5)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('<< (left shift)', () => {
|
|
||||||
test('basic left shift', () => {
|
|
||||||
expect('5 << 2').toEvaluateTo(20)
|
|
||||||
// 5 << 2 = 20
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shift by zero', () => {
|
|
||||||
expect('5 << 0').toEvaluateTo(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shift by one', () => {
|
|
||||||
expect('3 << 1').toEvaluateTo(6)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('large shift', () => {
|
|
||||||
expect('1 << 10').toEvaluateTo(1024)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('>> (signed right shift)', () => {
|
|
||||||
test('basic right shift', () => {
|
|
||||||
expect('20 >> 2').toEvaluateTo(5)
|
|
||||||
// 20 >> 2 = 5
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shift by zero', () => {
|
|
||||||
expect('20 >> 0').toEvaluateTo(20)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('preserves sign for negative numbers', () => {
|
|
||||||
expect('-20 >> 2').toEvaluateTo(-5)
|
|
||||||
// Sign is preserved
|
|
||||||
})
|
|
||||||
|
|
||||||
test('negative number right shift', () => {
|
|
||||||
expect('-8 >> 1').toEvaluateTo(-4)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('>>> (unsigned right shift)', () => {
|
|
||||||
test('basic unsigned right shift', () => {
|
|
||||||
expect('20 >>> 2').toEvaluateTo(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('unsigned shift of -1', () => {
|
|
||||||
expect('-1 >>> 1').toEvaluateTo(2147483647)
|
|
||||||
// -1 >>> 1 = 2147483647 (unsigned, no sign extension)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('unsigned shift of negative number', () => {
|
|
||||||
expect('-8 >>> 1').toEvaluateTo(2147483644)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('compound expressions', () => {
|
|
||||||
test('multiple bitwise operations', () => {
|
|
||||||
expect('(5 band 3) bor (8 bxor 12)').toEvaluateTo(5)
|
|
||||||
// (5 & 3) | (8 ^ 12) = 1 | 4 = 5
|
|
||||||
})
|
|
||||||
|
|
||||||
test('bitwise with variables', () => {
|
|
||||||
expect(`
|
|
||||||
a = 5
|
|
||||||
b = 3
|
|
||||||
a bor b
|
|
||||||
`).toEvaluateTo(7)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shift operations with variables', () => {
|
|
||||||
expect(`
|
|
||||||
x = 16
|
|
||||||
y = 2
|
|
||||||
x >> y
|
|
||||||
`).toEvaluateTo(4)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('mixing shifts and bitwise', () => {
|
|
||||||
expect('(8 << 1) band 15').toEvaluateTo(0)
|
|
||||||
// (8 << 1) & 15 = 16 & 15 = 0
|
|
||||||
})
|
|
||||||
|
|
||||||
test('mixing shifts and bitwise 2', () => {
|
|
||||||
expect('(7 << 1) band 15').toEvaluateTo(14)
|
|
||||||
// (7 << 1) & 15 = 14 & 15 = 14
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('precedence', () => {
|
|
||||||
test('bitwise has correct precedence with arithmetic', () => {
|
|
||||||
expect('1 + 2 band 3').toEvaluateTo(3)
|
|
||||||
// (1 + 2) & 3 = 3 & 3 = 3
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shift has correct precedence', () => {
|
|
||||||
expect('4 + 8 << 1').toEvaluateTo(24)
|
|
||||||
// (4 + 8) << 1 = 12 << 1 = 24
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -110,10 +110,7 @@ describe('compiler', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('function call with no args', () => {
|
test('function call with no args', () => {
|
||||||
expect(`bloop = do: 'bleep' end; bloop`).toEvaluateTo('bleep')
|
expect(`bloop = do: 'bloop' end; bloop`).toEvaluateTo('bloop')
|
||||||
expect(`bloop = [ go=do: 'bleep' end ]; bloop.go`).toEvaluateTo('bleep')
|
|
||||||
expect(`bloop = [ go=do: 'bleep' end ]; abc = do x: x end; abc (bloop.go)`).toEvaluateTo('bleep')
|
|
||||||
expect(`num = ((math.random) * 10 + 1) | math.floor; num >= 1 and num <= 10 `).toEvaluateTo(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('function call with if statement and multiple expressions', () => {
|
test('function call with if statement and multiple expressions', () => {
|
||||||
|
|
@ -301,23 +298,6 @@ describe('default params', () => {
|
||||||
expect('multiply = do x y=5: x * y end; multiply 5 2').toEvaluateTo(10)
|
expect('multiply = do x y=5: x * y end; multiply 5 2').toEvaluateTo(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('null triggers default value', () => {
|
|
||||||
expect('test = do n=true: n end; test').toEvaluateTo(true)
|
|
||||||
expect('test = do n=true: n end; test false').toEvaluateTo(false)
|
|
||||||
expect('test = do n=true: n end; test null').toEvaluateTo(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('null triggers default for named parameters', () => {
|
|
||||||
expect("greet = do name='World': name end; greet name=null").toEvaluateTo('World')
|
|
||||||
expect("greet = do name='World': name end; greet name='Bob'").toEvaluateTo('Bob')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('null triggers default with multiple parameters', () => {
|
|
||||||
expect('calc = do x=10 y=20: x + y end; calc null 5').toEvaluateTo(15)
|
|
||||||
expect('calc = do x=10 y=20: x + y end; calc 3 null').toEvaluateTo(23)
|
|
||||||
expect('calc = do x=10 y=20: x + y end; calc null null').toEvaluateTo(30)
|
|
||||||
})
|
|
||||||
|
|
||||||
test.skip('array default', () => {
|
test.skip('array default', () => {
|
||||||
expect('abc = do alpha=[a b c]: alpha end; abc').toEvaluateTo(['a', 'b', 'c'])
|
expect('abc = do alpha=[a b c]: alpha end; abc').toEvaluateTo(['a', 'b', 'c'])
|
||||||
expect('abc = do alpha=[a b c]: alpha end; abc [x y z]').toEvaluateTo(['x', 'y', 'z'])
|
expect('abc = do alpha=[a b c]: alpha end; abc [x y z]').toEvaluateTo(['x', 'y', 'z'])
|
||||||
|
|
@ -333,118 +313,3 @@ describe('default params', () => {
|
||||||
).toEvaluateTo({ name: 'Jon', age: 21 })
|
).toEvaluateTo({ name: 'Jon', age: 21 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Nullish coalescing operator (??)', () => {
|
|
||||||
test('returns left side when not null', () => {
|
|
||||||
expect('5 ?? 10').toEvaluateTo(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns right side when left is null', () => {
|
|
||||||
expect('null ?? 10').toEvaluateTo(10)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns left side when left is false', () => {
|
|
||||||
expect('false ?? 10').toEvaluateTo(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns left side when left is 0', () => {
|
|
||||||
expect('0 ?? 10').toEvaluateTo(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns left side when left is empty string', () => {
|
|
||||||
expect(`'' ?? 'default'`).toEvaluateTo('')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('chains left to right', () => {
|
|
||||||
expect('null ?? null ?? 42').toEvaluateTo(42)
|
|
||||||
expect('null ?? 10 ?? 20').toEvaluateTo(10)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('short-circuits evaluation', () => {
|
|
||||||
const throwError = () => { throw new Error('Should not evaluate') }
|
|
||||||
expect('5 ?? throw-error').toEvaluateTo(5, { 'throw-error': throwError })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('works with variables', () => {
|
|
||||||
expect('x = null; x ?? 5').toEvaluateTo(5)
|
|
||||||
expect('y = 3; y ?? 5').toEvaluateTo(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('works with function calls', () => {
|
|
||||||
const getValue = () => null
|
|
||||||
const getDefault = () => 42
|
|
||||||
// Note: identifiers without parentheses refer to the function, not call it
|
|
||||||
// Use explicit call syntax to invoke the function
|
|
||||||
expect('(get-value) ?? (get-default)').toEvaluateTo(42, {
|
|
||||||
'get-value': getValue,
|
|
||||||
'get-default': getDefault
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Nullish coalescing assignment (??=)', () => {
|
|
||||||
test('assigns when variable is null', () => {
|
|
||||||
expect('x = null; x ??= 5; x').toEvaluateTo(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not assign when variable is not null', () => {
|
|
||||||
expect('x = 3; x ??= 10; x').toEvaluateTo(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not assign when variable is false', () => {
|
|
||||||
expect('x = false; x ??= true; x').toEvaluateTo(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not assign when variable is 0', () => {
|
|
||||||
expect('x = 0; x ??= 100; x').toEvaluateTo(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not assign when variable is empty string', () => {
|
|
||||||
expect(`x = ''; x ??= 'default'; x`).toEvaluateTo('')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns the final value', () => {
|
|
||||||
expect('x = null; x ??= 5').toEvaluateTo(5)
|
|
||||||
expect('y = 3; y ??= 10').toEvaluateTo(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('short-circuits evaluation when not null', () => {
|
|
||||||
const throwError = () => { throw new Error('Should not evaluate') }
|
|
||||||
expect('x = 5; x ??= throw-error; x').toEvaluateTo(5, { 'throw-error': throwError })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('works with expressions', () => {
|
|
||||||
expect('x = null; x ??= 2 + 3; x').toEvaluateTo(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('works with function calls', () => {
|
|
||||||
const getDefault = () => 42
|
|
||||||
expect('x = null; x ??= (get-default); x').toEvaluateTo(42, { 'get-default': getDefault })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('throws when variable is undefined', () => {
|
|
||||||
expect(() => expect('undefined-var ??= 5').toEvaluateTo(null)).toThrow()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Compound assignment operators', () => {
|
|
||||||
test('+=', () => {
|
|
||||||
expect('x = 5; x += 3; x').toEvaluateTo(8)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('-=', () => {
|
|
||||||
expect('x = 10; x -= 4; x').toEvaluateTo(6)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('*=', () => {
|
|
||||||
expect('x = 3; x *= 4; x').toEvaluateTo(12)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('/=', () => {
|
|
||||||
expect('x = 20; x /= 5; x').toEvaluateTo(4)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('%=', () => {
|
|
||||||
expect('x = 10; x %= 3; x').toEvaluateTo(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,6 @@
|
||||||
import { describe } from 'bun:test'
|
import { describe } from 'bun:test'
|
||||||
import { expect, test } from 'bun:test'
|
import { expect, test } from 'bun:test'
|
||||||
|
|
||||||
describe('number literals', () => {
|
|
||||||
test('binary literals', () => {
|
|
||||||
expect('0b110').toEvaluateTo(6)
|
|
||||||
expect('0b1010').toEvaluateTo(10)
|
|
||||||
expect('0b11111111').toEvaluateTo(255)
|
|
||||||
expect('0b0').toEvaluateTo(0)
|
|
||||||
expect('0b1').toEvaluateTo(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('hex literals', () => {
|
|
||||||
expect('0xdeadbeef').toEvaluateTo(0xdeadbeef)
|
|
||||||
expect('0xdeadbeef').toEvaluateTo(3735928559)
|
|
||||||
expect('0xFF').toEvaluateTo(255)
|
|
||||||
expect('0xff').toEvaluateTo(255)
|
|
||||||
expect('0x10').toEvaluateTo(16)
|
|
||||||
expect('0x0').toEvaluateTo(0)
|
|
||||||
expect('0xABCDEF').toEvaluateTo(0xabcdef)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('decimal literals still work', () => {
|
|
||||||
expect('42').toEvaluateTo(42)
|
|
||||||
expect('3.14').toEvaluateTo(3.14)
|
|
||||||
expect('0').toEvaluateTo(0)
|
|
||||||
expect('999999').toEvaluateTo(999999)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('negative hex and binary', () => {
|
|
||||||
expect('-0xFF').toEvaluateTo(-255)
|
|
||||||
expect('-0b1010').toEvaluateTo(-10)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('positive prefix', () => {
|
|
||||||
expect('+0xFF').toEvaluateTo(255)
|
|
||||||
expect('+0b110').toEvaluateTo(6)
|
|
||||||
expect('+42').toEvaluateTo(42)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('array literals', () => {
|
describe('array literals', () => {
|
||||||
test('work with numbers', () => {
|
test('work with numbers', () => {
|
||||||
expect('[1 2 3]').toEvaluateTo([1, 2, 3])
|
expect('[1 2 3]').toEvaluateTo([1, 2, 3])
|
||||||
|
|
|
||||||
|
|
@ -92,30 +92,4 @@ describe('pipe expressions', () => {
|
||||||
get-msg | length
|
get-msg | length
|
||||||
`).toEvaluateTo(5)
|
`).toEvaluateTo(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
test('string literals can be piped', () => {
|
|
||||||
expect(`'hey there' | str.to-upper`).toEvaluateTo('HEY THERE')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('number literals can be piped', () => {
|
|
||||||
expect(`42 | str.trim`).toEvaluateTo('42')
|
|
||||||
expect(`4.22 | str.trim`).toEvaluateTo('4.22')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('null literals can be piped', () => {
|
|
||||||
expect(`null | type`).toEvaluateTo('null')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('boolean literals can be piped', () => {
|
|
||||||
expect(`true | str.to-upper`).toEvaluateTo('TRUE')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('array literals can be piped', () => {
|
|
||||||
expect(`[1 2 3] | str.join '-'`).toEvaluateTo('1-2-3')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('dict literals can be piped', () => {
|
|
||||||
expect(`[a=1 b=2 c=3] | dict.values | list.sort | str.join '-'`).toEvaluateTo('1-2-3')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
67
src/index.ts
67
src/index.ts
|
|
@ -1,17 +1,11 @@
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { VM, fromValue, toValue, isValue, type Bytecode } from 'reefvm'
|
import { VM, fromValue, type Bytecode } from 'reefvm'
|
||||||
import { type Tree } from '@lezer/common'
|
|
||||||
import { Compiler } from '#compiler/compiler'
|
import { Compiler } from '#compiler/compiler'
|
||||||
import { parser } from '#parser/shrimp'
|
import { globals as shrimpGlobals, colors } from '#prelude'
|
||||||
import { globals as parserGlobals, setGlobals as setParserGlobals } from '#parser/tokenizer'
|
|
||||||
import { globals as shrimpGlobals } from '#prelude'
|
|
||||||
|
|
||||||
export { Compiler } from '#compiler/compiler'
|
export { Compiler } from '#compiler/compiler'
|
||||||
export { parser } from '#parser/shrimp'
|
export { parser } from '#parser/shrimp'
|
||||||
export { globals as prelude } from '#prelude'
|
export { globals } from '#prelude'
|
||||||
export type { Tree } from '@lezer/common'
|
|
||||||
export { type Value, type Bytecode } from 'reefvm'
|
|
||||||
export { toValue, fromValue, isValue, Scope, VM, bytecodeToString } from 'reefvm'
|
|
||||||
|
|
||||||
export class Shrimp {
|
export class Shrimp {
|
||||||
vm: VM
|
vm: VM
|
||||||
|
|
@ -23,32 +17,6 @@ export class Shrimp {
|
||||||
this.globals = globals
|
this.globals = globals
|
||||||
}
|
}
|
||||||
|
|
||||||
get(name: string): any {
|
|
||||||
const value = this.vm.scope.get(name)
|
|
||||||
return value ? fromValue(value, this.vm) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
set(name: string, value: any) {
|
|
||||||
this.vm.scope.set(name, toValue(value, this.vm))
|
|
||||||
}
|
|
||||||
|
|
||||||
has(name: string): boolean {
|
|
||||||
return this.vm.scope.has(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
async call(name: string, ...args: any[]): Promise<any> {
|
|
||||||
const result = await this.vm.call(name, ...args)
|
|
||||||
return isValue(result) ? fromValue(result, this.vm) : result
|
|
||||||
}
|
|
||||||
|
|
||||||
parse(code: string): Tree {
|
|
||||||
return parseCode(code, this.globals)
|
|
||||||
}
|
|
||||||
|
|
||||||
compile(code: string): Bytecode {
|
|
||||||
return compileCode(code, this.globals)
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(code: string | Bytecode, locals?: Record<string, any>): Promise<any> {
|
async run(code: string | Bytecode, locals?: Record<string, any>): Promise<any> {
|
||||||
let bytecode
|
let bytecode
|
||||||
|
|
||||||
|
|
@ -64,9 +32,13 @@ export class Shrimp {
|
||||||
await this.vm.continue()
|
await this.vm.continue()
|
||||||
if (locals) this.vm.popScope()
|
if (locals) this.vm.popScope()
|
||||||
|
|
||||||
return this.vm.stack.length ? fromValue(this.vm.stack.at(-1)!, this.vm) : null
|
return this.vm.stack.length ? fromValue(this.vm.stack.at(-1)!) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get(name: string): any {
|
||||||
|
const value = this.vm.scope.get(name)
|
||||||
|
return value ? fromValue(value) : null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runFile(path: string, globals?: Record<string, any>): Promise<any> {
|
export async function runFile(path: string, globals?: Record<string, any>): Promise<any> {
|
||||||
|
|
@ -79,9 +51,14 @@ export async function runCode(code: string, globals?: Record<string, any>): Prom
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runBytecode(bytecode: Bytecode, globals?: Record<string, any>): Promise<any> {
|
export async function runBytecode(bytecode: Bytecode, globals?: Record<string, any>): Promise<any> {
|
||||||
|
try {
|
||||||
const vm = new VM(bytecode, Object.assign({}, shrimpGlobals, globals))
|
const vm = new VM(bytecode, Object.assign({}, shrimpGlobals, globals))
|
||||||
await vm.run()
|
await vm.run()
|
||||||
return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]!, vm) : null
|
return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]!) : null
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`${colors.red}Error:${colors.reset} ${error.message}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compileFile(path: string, globals?: Record<string, any>): Bytecode {
|
export function compileFile(path: string, globals?: Record<string, any>): Bytecode {
|
||||||
|
|
@ -94,19 +71,3 @@ export function compileCode(code: string, globals?: Record<string, any>): Byteco
|
||||||
const compiler = new Compiler(code, globalNames)
|
const compiler = new Compiler(code, globalNames)
|
||||||
return compiler.bytecode
|
return compiler.bytecode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseFile(path: string, globals?: Record<string, any>): Tree {
|
|
||||||
const code = readFileSync(path, 'utf-8')
|
|
||||||
return parseCode(code, globals)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseCode(code: string, globals?: Record<string, any>): Tree {
|
|
||||||
const oldGlobals = [...parserGlobals]
|
|
||||||
const globalNames = [...Object.keys(shrimpGlobals), ...(globals ? Object.keys(globals) : [])]
|
|
||||||
|
|
||||||
setParserGlobals(globalNames)
|
|
||||||
const result = parser.parse(code)
|
|
||||||
setParserGlobals(oldGlobals)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
@ -5,28 +5,18 @@ type Operator = { str: string; tokenName: keyof typeof terms }
|
||||||
const operators: Array<Operator> = [
|
const operators: Array<Operator> = [
|
||||||
{ str: 'and', tokenName: 'And' },
|
{ str: 'and', tokenName: 'And' },
|
||||||
{ str: 'or', tokenName: 'Or' },
|
{ str: 'or', tokenName: 'Or' },
|
||||||
{ str: 'band', tokenName: 'Band' },
|
|
||||||
{ str: 'bor', tokenName: 'Bor' },
|
|
||||||
{ str: 'bxor', tokenName: 'Bxor' },
|
|
||||||
{ str: '>>>', tokenName: 'Ushr' }, // Must come before >>
|
|
||||||
{ str: '>>', tokenName: 'Shr' },
|
|
||||||
{ str: '<<', tokenName: 'Shl' },
|
|
||||||
{ str: '>=', tokenName: 'Gte' },
|
{ str: '>=', tokenName: 'Gte' },
|
||||||
{ str: '<=', tokenName: 'Lte' },
|
{ str: '<=', tokenName: 'Lte' },
|
||||||
{ str: '!=', tokenName: 'Neq' },
|
{ str: '!=', tokenName: 'Neq' },
|
||||||
{ str: '==', tokenName: 'EqEq' },
|
{ str: '==', tokenName: 'EqEq' },
|
||||||
|
|
||||||
// Compound assignment operators (must come before single-char operators)
|
// Compound assignment operators (must come before single-char operators)
|
||||||
{ str: '??=', tokenName: 'NullishEq' },
|
|
||||||
{ str: '+=', tokenName: 'PlusEq' },
|
{ str: '+=', tokenName: 'PlusEq' },
|
||||||
{ str: '-=', tokenName: 'MinusEq' },
|
{ str: '-=', tokenName: 'MinusEq' },
|
||||||
{ str: '*=', tokenName: 'StarEq' },
|
{ str: '*=', tokenName: 'StarEq' },
|
||||||
{ str: '/=', tokenName: 'SlashEq' },
|
{ str: '/=', tokenName: 'SlashEq' },
|
||||||
{ str: '%=', tokenName: 'ModuloEq' },
|
{ str: '%=', tokenName: 'ModuloEq' },
|
||||||
|
|
||||||
// Nullish coalescing (must come before it could be mistaken for other tokens)
|
|
||||||
{ str: '??', tokenName: 'NullishCoalesce' },
|
|
||||||
|
|
||||||
// Single-char operators
|
// Single-char operators
|
||||||
{ str: '*', tokenName: 'Star' },
|
{ str: '*', tokenName: 'Star' },
|
||||||
{ str: '=', tokenName: 'Eq' },
|
{ str: '=', tokenName: 'Eq' },
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,20 @@
|
||||||
|
|
||||||
@top Program { item* }
|
@top Program { item* }
|
||||||
|
|
||||||
@external tokens operatorTokenizer from "./operatorTokenizer" { 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 }
|
@external tokens operatorTokenizer from "./operatorTokenizer" { Star, Slash, Plus, Minus, And, Or, Eq, EqEq, Neq, Lt, Lte, Gt, Gte, Modulo, PlusEq, MinusEq, StarEq, SlashEq, ModuloEq }
|
||||||
|
|
||||||
@tokens {
|
@tokens {
|
||||||
@precedence { Number Regex }
|
@precedence { Number Regex }
|
||||||
|
|
||||||
StringFragment { !['\\$]+ }
|
StringFragment { !['\\$]+ }
|
||||||
DoubleQuote { '"' !["]* '"' }
|
DoubleQuote { '"' !["]* '"' }
|
||||||
NamedArgPrefix { $[a-z] $[a-z0-9-]* "=" }
|
NamedArgPrefix { $[a-z-]+ "=" }
|
||||||
Number {
|
Number { ("-" | "+")? $[0-9]+ ('.' $[0-9]+)? }
|
||||||
("-" | "+")? "0x" $[0-9a-fA-F]+ |
|
|
||||||
("-" | "+")? "0b" $[01]+ |
|
|
||||||
("-" | "+")? $[0-9]+ ("_"? $[0-9]+)* ('.' $[0-9]+ ("_"? $[0-9]+)*)?
|
|
||||||
}
|
|
||||||
Boolean { "true" | "false" }
|
Boolean { "true" | "false" }
|
||||||
newlineOrSemicolon { "\n" | ";" }
|
newlineOrSemicolon { "\n" | ";" }
|
||||||
eof { @eof }
|
eof { @eof }
|
||||||
space { " " | "\t" }
|
space { " " | "\t" }
|
||||||
Comment { "#" ![\n]* }
|
Comment { "#" " " ![\n]* }
|
||||||
leftParen { "(" }
|
leftParen { "(" }
|
||||||
rightParen { ")" }
|
rightParen { ")" }
|
||||||
colon[closedBy="end", @name="colon"] { ":" }
|
colon[closedBy="end", @name="colon"] { ":" }
|
||||||
|
|
@ -49,13 +45,10 @@ null { @specialize[@name=Null]<Identifier, "null"> }
|
||||||
pipe @left,
|
pipe @left,
|
||||||
or @left,
|
or @left,
|
||||||
and @left,
|
and @left,
|
||||||
nullish @left,
|
|
||||||
comparison @left,
|
comparison @left,
|
||||||
multiplicative @left,
|
multiplicative @left,
|
||||||
additive @left,
|
additive @left,
|
||||||
bitwise @left,
|
call
|
||||||
call,
|
|
||||||
functionWithNewlines
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
|
|
@ -85,7 +78,7 @@ PipeExpr {
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeOperand {
|
pipeOperand {
|
||||||
consumeToTerminator
|
FunctionCall | FunctionCallOrIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
WhileExpr {
|
WhileExpr {
|
||||||
|
|
@ -168,8 +161,7 @@ ConditionalOp {
|
||||||
expression !comparison Gt expression |
|
expression !comparison Gt expression |
|
||||||
expression !comparison Gte expression |
|
expression !comparison Gte expression |
|
||||||
(expression | ConditionalOp) !and And (expression | ConditionalOp) |
|
(expression | ConditionalOp) !and And (expression | ConditionalOp) |
|
||||||
(expression | ConditionalOp) !or Or (expression | ConditionalOp) |
|
(expression | ConditionalOp) !or Or (expression | ConditionalOp)
|
||||||
(expression | ConditionalOp) !nullish NullishCoalesce (expression | ConditionalOp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Params {
|
Params {
|
||||||
|
|
@ -185,7 +177,7 @@ Assign {
|
||||||
}
|
}
|
||||||
|
|
||||||
CompoundAssign {
|
CompoundAssign {
|
||||||
AssignableIdentifier (PlusEq | MinusEq | StarEq | SlashEq | ModuloEq | NullishEq) consumeToTerminator
|
AssignableIdentifier (PlusEq | MinusEq | StarEq | SlashEq | ModuloEq) consumeToTerminator
|
||||||
}
|
}
|
||||||
|
|
||||||
BinOp {
|
BinOp {
|
||||||
|
|
@ -193,31 +185,11 @@ BinOp {
|
||||||
(expression | BinOp) !multiplicative Star (expression | BinOp) |
|
(expression | BinOp) !multiplicative Star (expression | BinOp) |
|
||||||
(expression | BinOp) !multiplicative Slash (expression | BinOp) |
|
(expression | BinOp) !multiplicative Slash (expression | BinOp) |
|
||||||
(expression | BinOp) !additive Plus (expression | BinOp) |
|
(expression | BinOp) !additive Plus (expression | BinOp) |
|
||||||
(expression | BinOp) !additive Minus (expression | BinOp) |
|
(expression | BinOp) !additive Minus (expression | BinOp)
|
||||||
(expression | BinOp) !bitwise Band (expression | BinOp) |
|
|
||||||
(expression | BinOp) !bitwise Bor (expression | BinOp) |
|
|
||||||
(expression | BinOp) !bitwise Bxor (expression | BinOp) |
|
|
||||||
(expression | BinOp) !bitwise Shl (expression | BinOp) |
|
|
||||||
(expression | BinOp) !bitwise Shr (expression | BinOp) |
|
|
||||||
(expression | BinOp) !bitwise Ushr (expression | BinOp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ParenExpr {
|
ParenExpr {
|
||||||
leftParen newlineOrSemicolon* (
|
leftParen (IfExpr | ambiguousFunctionCall | BinOp | expressionWithoutIdentifier | ConditionalOp | PipeExpr | FunctionDef) rightParen
|
||||||
FunctionCallWithNewlines |
|
|
||||||
IfExpr |
|
|
||||||
ambiguousFunctionCall |
|
|
||||||
BinOp newlineOrSemicolon* |
|
|
||||||
expressionWithoutIdentifier |
|
|
||||||
ConditionalOp newlineOrSemicolon* |
|
|
||||||
PipeExpr |
|
|
||||||
FunctionDef
|
|
||||||
)
|
|
||||||
rightParen
|
|
||||||
}
|
|
||||||
|
|
||||||
FunctionCallWithNewlines[@name=FunctionCall] {
|
|
||||||
(DotGet | Identifier | ParenExpr) newlineOrSemicolon+ arg !functionWithNewlines (newlineOrSemicolon+ arg)* newlineOrSemicolon*
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expression {
|
expression {
|
||||||
|
|
|
||||||
|
|
@ -19,59 +19,50 @@ export const
|
||||||
StarEq = 17,
|
StarEq = 17,
|
||||||
SlashEq = 18,
|
SlashEq = 18,
|
||||||
ModuloEq = 19,
|
ModuloEq = 19,
|
||||||
Band = 20,
|
Identifier = 20,
|
||||||
Bor = 21,
|
AssignableIdentifier = 21,
|
||||||
Bxor = 22,
|
Word = 22,
|
||||||
Shl = 23,
|
IdentifierBeforeDot = 23,
|
||||||
Shr = 24,
|
CurlyString = 24,
|
||||||
Ushr = 25,
|
Do = 25,
|
||||||
NullishCoalesce = 26,
|
Comment = 26,
|
||||||
NullishEq = 27,
|
Program = 27,
|
||||||
Identifier = 28,
|
PipeExpr = 28,
|
||||||
AssignableIdentifier = 29,
|
FunctionCall = 29,
|
||||||
Word = 30,
|
DotGet = 30,
|
||||||
IdentifierBeforeDot = 31,
|
Number = 31,
|
||||||
CurlyString = 32,
|
ParenExpr = 32,
|
||||||
Do = 33,
|
IfExpr = 33,
|
||||||
Comment = 34,
|
keyword = 72,
|
||||||
Program = 35,
|
ConditionalOp = 35,
|
||||||
PipeExpr = 36,
|
String = 36,
|
||||||
WhileExpr = 38,
|
StringFragment = 37,
|
||||||
keyword = 81,
|
Interpolation = 38,
|
||||||
ConditionalOp = 40,
|
EscapeSeq = 39,
|
||||||
ParenExpr = 41,
|
DoubleQuote = 40,
|
||||||
FunctionCallWithNewlines = 42,
|
Boolean = 41,
|
||||||
DotGet = 43,
|
Regex = 42,
|
||||||
Number = 44,
|
Dict = 43,
|
||||||
PositionalArg = 45,
|
NamedArg = 44,
|
||||||
|
NamedArgPrefix = 45,
|
||||||
FunctionDef = 46,
|
FunctionDef = 46,
|
||||||
Params = 47,
|
Params = 47,
|
||||||
NamedParam = 48,
|
NamedParam = 48,
|
||||||
NamedArgPrefix = 49,
|
Null = 49,
|
||||||
String = 50,
|
colon = 50,
|
||||||
StringFragment = 51,
|
CatchExpr = 51,
|
||||||
Interpolation = 52,
|
Block = 53,
|
||||||
EscapeSeq = 53,
|
FinallyExpr = 54,
|
||||||
DoubleQuote = 54,
|
Underscore = 57,
|
||||||
Boolean = 55,
|
Array = 58,
|
||||||
Null = 56,
|
ElseIfExpr = 59,
|
||||||
colon = 57,
|
ElseExpr = 61,
|
||||||
CatchExpr = 58,
|
FunctionCallOrIdentifier = 62,
|
||||||
Block = 60,
|
BinOp = 63,
|
||||||
FinallyExpr = 61,
|
PositionalArg = 64,
|
||||||
Underscore = 64,
|
WhileExpr = 66,
|
||||||
NamedArg = 65,
|
FunctionCallWithBlock = 68,
|
||||||
IfExpr = 66,
|
TryExpr = 69,
|
||||||
FunctionCall = 68,
|
Throw = 71,
|
||||||
ElseIfExpr = 69,
|
CompoundAssign = 73,
|
||||||
ElseExpr = 71,
|
Assign = 74
|
||||||
FunctionCallOrIdentifier = 72,
|
|
||||||
BinOp = 73,
|
|
||||||
Regex = 74,
|
|
||||||
Dict = 75,
|
|
||||||
Array = 76,
|
|
||||||
FunctionCallWithBlock = 77,
|
|
||||||
TryExpr = 78,
|
|
||||||
Throw = 80,
|
|
||||||
CompoundAssign = 82,
|
|
||||||
Assign = 83
|
|
||||||
|
|
|
||||||
|
|
@ -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,if:68, null:98, catch:104, finally:110, end:112, else:120, while:134, try:140, throw:144}
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "<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: "9bQYQbOOO!jOSO'#DQOOQa'#DQ'#DQOOQa'#DX'#DXO#yQbO'#DhO%_QcO'#E`OOQa'#E`'#E`O&kQcO'#E`O'mQcO'#E_O(TQcO'#E_O)vQRO'#DPO+VQcO'#EYO+aQcO'#EYO+qQbO'#C|O,rOpO'#CzOOQ`'#EZ'#EZO,wQbO'#EYO-RQRO'#DwOOQ`'#EY'#EYO-gQQO'#EXOOQ`'#EX'#EXOOQ`'#Dy'#DyQYQbOOO-oQbO'#D[O-zQbO'#C}O.uQbO'#DpO/pQQO'#DsO.uQbO'#DuO/uQbO'#DSO/}QWO'#DTOOOO'#Eb'#EbOOOO'#Dz'#DzO0cOSO,59lOOQa,59l,59lOOQ`'#D{'#D{O0qQbO,5:SO0xQbO'#DYO1SQQO,59sOOQa,5:S,5:SO1_QbO,5:SOOQa'#E_'#E_OOQ`'#Dn'#DnOOQ`'#En'#EnOOQ`'#ES'#ESO1iQbO,59eO2cQbO,5:dO.uQbO,59kO.uQbO,59kO.uQbO,59kO.uQbO,5:XO.uQbO,5:XO.uQbO,5:XO2sQRO,59hO2zQRO,59hO3VQRO,59hO3QQQO,59hO3hQQO,59hO3pObO,59fO3{QbO'#ETO4WQbO,59dO4rQbO,5:^O2cQbO,5:cOOQ`,5:s,5:sOOQ`-E7w-E7wOOQ`'#D|'#D|O5VQbO'#D]O5bQbO'#D^OOQO'#D}'#D}O5YQQO'#D]O5vQQO,59vO5{QcO'#E_O7aQRO'#E^O7hQRO'#E^OOQO'#E^'#E^O7sQQO,59iO7xQRO,5:[O8PQRO,5:[O4rQbO,5:_O8[QcO,5:aO9WQcO,5:aO9bQcO,5:aOOOO,59n,59nOOOO,59o,59oOOOO-E7x-E7xOOQa1G/W1G/WOOQ`-E7y-E7yO9rQQO1G/_OOQa1G/n1G/nO9}QbO1G/nOOQ`,59t,59tOOQO'#EP'#EPO9rQQO1G/_OOQa1G/_1G/_OOQ`'#EQ'#EQO9}QbO1G/nOOQ`-E8Q-E8QOOQ`1G0O1G0OOOQa1G/V1G/VO;YQcO1G/VO;aQcO1G/VO;hQcO1G/VOOQa1G/s1G/sO<aQcO1G/sO<kQcO1G/sO<uQcO1G/sOOQa1G/S1G/SOOQa1G/Q1G/QO=jQbO'#DlO>aQbO'#CyOOQ`,5:o,5:oOOQ`-E8R-E8ROOQ`'#Dc'#DcO>nQbO'#DcO?_QbO1G/xOOQ`1G/}1G/}OOQ`-E7z-E7zO?jQQO,59wOOQO,59x,59xOOQO-E7{-E7{O?rQbO1G/bO4rQbO1G/TO4rQbO1G/vO@VQbO1G/yO@bQQO7+$yOOQa7+$y7+$yO@mQbO7+%YOOQa7+%Y7+%YOOQO-E7}-E7}OOQ`-E8O-E8OOOQ`'#EO'#EOO@wQQO'#EOO@|QbO'#EkOOQ`,59},59}OAmQbO'#DaOArQQO'#DdOOQ`7+%d7+%dOAwQbO7+%dOA|QbO7+%dOBUQbO7+$|OBaQbO7+$|OB}QbO7+$oOCVQbO7+%bOOQ`7+%e7+%eOC[QbO7+%eOCaQbO7+%eOOQa<<He<<HeOOQa<<Ht<<HtOOQ`,5:j,5:jOOQ`-E7|-E7|OCiQQO,59{O4rQbO,5:OOOQ`<<IO<<IOOCnQbO<<IOOOQ`<<Hh<<HhOCsQbO<<HhOCxQbO<<HhODQQbO<<HhOOQ`'#ER'#EROD]QbO<<HZODeQbO'#DkOOQ`<<HZ<<HZODmQbO<<HZOOQ`<<H|<<H|OOQ`<<IP<<IPODrQbO<<IPO4rQbO1G/gOOQ`1G/j1G/jOOQ`AN>jAN>jOOQ`AN>SAN>SODwQbOAN>SOD|QbOAN>SOOQ`-E8P-E8POOQ`AN=uAN=uOEUQbOAN=uO-zQbO,5:TO4rQbO,5:VOOQ`AN>kAN>kOOQ`7+%R7+%ROOQ`G23nG23nOEZQbOG23nPE`QbO'#DiOOQ`G23aG23aOEeQQO1G/oOOQ`1G/q1G/qOOQ`LD)YLD)YO4rQbO7+%ZOOQ`<<Hu<<Hu",
|
||||||
stateData: "!$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: "Em~O!zOSjOS~OdXOeaOfUOg^OhQOigOoUOrhOxQOyUOzUO!RUO!eiO!hjO!jkO#P]O#TPO#[RO#]SO#^dO~OunO#TqO#VlO#WmO~OdxOfUOg^OhQOoUOxQOyUOzUO}tO!RUO#P]O#TPO#[RO#]SO#^rO~O#`vO~P!xOP#SXQ#SXR#SXS#SXT#SXU#SXW#SXX#SXY#SXZ#SX[#SX]#SX^#SX#^#SX#c#SX!U#SX!X#SX!Y#SX!^#SX~OdxOfUOg^OhQOigOoUOxQOyUOzUO}tO!RUO!ZyO#P]O#TPO#[RO#]SO#a#SX!S#SX~P$QOV}O~P$QOP#RXQ#RXR#RXS#RXT#RXU#RXW#RXX#RXY#RXZ#RX[#RX]#RX^#RX~O#^!|X#c!|X!U!|X!X!|X!Y!|X!^!|X~P&rOdxOfUOg^OhQOigOoUOxQOyUOzUO}tO!RUO!ZyO#P]O#TPO#[RO#]SO!S!`X!c!`X#^!`X#c!`X#a!`X!U!`X!X!`X!Y!`X!^!`X~P&rOP!SOQ!SOR!TOS!TOT!POU!QOW!OOX!OOY!OOZ!OO[!OO]!OO^!RO~O#^!|X#c!|X!U!|X!X!|X!Y!|X!^!|X~OT!POU!QO~P*qOP!SOQ!SOR!TOS!TO~P*qOdXOfUOg^OhQOigOoUOrhOxQOyUOzUO!RUO#P]O#TPO#[RO#]SO~O#O!ZO~O!S!^O!c![O~P*qOV}O_!_O`!_Oa!_Ob!_Oc!_O~O#^!`O#c!`O~Od!bO}!dO!S!PP~Od!hOfUOg^OhQOoUOxQOyUOzUO!RUO#P]O#TPO#[RO#]SO~OdxOfUOg^OhQOoUOxQOyUOzUO!RUO#P]O#TPO#[RO#]SO~O!S!oO~Od!sO#P]O~O#T!tO#V!tO#W!tO#X!tO#Y!tO#Z!tO~OunO#T!vO#VlO#WmO~O#`!yO~P!xOigO!Z!{O~P.uO}tO#^!|O#`#OO~O#^#PO#`!yO~P.uOigO}tO!ZyO!Sma!cma#^ma#cma#ama!Uma!Xma!Yma!^ma~P.uOeaO!eiO!hjO!jkO~P+qO#a#]O~P&rOT!POU!QO#a#]O~OP!SOQ!SOR!TOS!TO#a#]O~O!c![O#a#]O~Od#^Oo#^O#P]O~Od#_Og^O#P]O~O!c![O#^la#cla#ala!Ula!Xla!Yla!^la~OeaO!eiO!hjO!jkO#^#dO~P+qOd!bO}!dO!S!PX~OhQOo#iOxQOy#iO!R#iO#TPO~O!S#kO~OigO}tO!ZyOT#RXU#RXW#RXX#RXY#RXZ#RX[#RX]#RX!S#RX~P.uOT!POU!QOW!OOX!OOY!OOZ!OO[!OO]!OO~O!S#QX~P6uOT!POU!QO!S#QX~O!S#lO~O!S#mO~P6uOT!POU!QO!S#mO~O#^!ia#c!ia!U!ia!X!ia!Y!ia!^!ia~P)vO#^!ia#c!ia!U!ia!X!ia!Y!ia!^!ia~OT!POU!QO~P8rOP!SOQ!SOR!TOS!TO~P8rO}tO#^!|O#`#pO~O#^#PO#`#rO~P.uOW!OOX!OOY!OOZ!OO[!OO]!OOTsi#^si#csi#asi!Ssi!Usi!Xsi!Ysi!^si~OU!QO~P:XOU!QO~P:kOUsi~P:XO^!ROR!aiS!ai#^!ai#c!ai#a!ai!U!ai!X!ai!Y!ai!^!ai~OP!aiQ!ai~P;oOP!SOQ!SO~P;oOP!SOQ!SOR!aiS!ai#^!ai#c!ai#a!ai!U!ai!X!ai!Y!ai!^!ai~OigO}tO!ZyO!c!`X#^!`X#c!`X#a!`X!U!`X!X!`X!Y!`X!^!`X~P.uOigO}tO!ZyO~P.uOeaO!eiO!hjO!jkO#^#uO!U#_P!X#_P!Y#_P!^#_P~P+qO!U#yO!X#zO!Y#{O~O}!dO!S!Pa~OeaO!eiO!hjO!jkO#^$PO~P+qO!U#yO!X#zO!Y$SO~O}tO#^!|O#`$VO~O#^#PO#`$WO~P.uO#^$XO~OeaO!eiO!hjO!jkO#^#uO!U#_X!X#_X!Y#_X!^#_X~P+qOd$ZO~O!S$[O~O!Y$]O~O!X#zO!Y$]O~O!U#yO!X#zO!Y$_O~OeaO!eiO!hjO!jkO#^#uO!U#_P!X#_P!Y#_P~P+qO!Y$fO!^$eO~O!Y$hO~O!Y$iO~O!X#zO!Y$iO~O!S$kO~O!Y$mO~O!Y$nO~O!X#zO!Y$nO~O!U#yO!X#zO!Y$nO~O!Y$rO!^$eO~Or$tO!S$uO~O!Y$rO~O!Y$vO~O!Y$xO~O!X#zO!Y$xO~O!Y${O~O!Y%OO~Or$tO~O!S%PO~Ooz~",
|
||||||
goto: "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: "4z#cPPPPPPPPPPPPPPPPPPPPPPPPPPPP#d#y$cP%f#dP&m'dP(c(cPPP(g)cP)w*i*lPP*rP+O+hPPP,O,|P-Q-W-l.[P.dP.d.dP.dP.d.d.v.|/S/Y/`/j/q/{0V0]0gPPP0n0r1`PP1x2O3hP4hPPPPPPPP4lPP4rpbOf}!^!_!o#d#k#l#m#w$P$[$k$u%PR!X]t_O]f}![!^!_!o#d#k#l#m#w$P$[$k$u%PT!kh$trXO]f}!^!_!o#d#k#l#m#w$P$[$k$u%PzxSTXikstw|!O!P!Q!R!S!T!h!z#Q#_#`#qS!hh$tR#_![vTO]fh}!^!_!o#d#k#l#m#w$P$[$k$t$u%PzUSTXikstw|!O!P!Q!R!S!T!h!z#Q#_#`#qQ!slQ#^!ZR#`![pZOf}!^!_!o#d#k#l#m#w$P$[$k$u%PQ!V]S!jh$tQ!niQ!qkQ#T!QR#V!P!rUOSTX]fhikstw|}!O!P!Q!R!S!T!^!_!h!o!z#Q#_#`#d#k#l#m#q#w$P$[$k$t$u%PR#i!dTnPp!sUOSTX]fhikstw|}!O!P!Q!R!S!T!^!_!h!o!z#Q#_#`#d#k#l#m#q#w$P$[$k$t$u%PQuS[zTX|!h#_#`Q!xsX!|u!x!}#opbOf}!^!_!o#d#k#l#m#w$P$[$k$u%P[yTX|!h#_#`Q!X]R!{tR!ggX!eg!c!f#hQ#}#eQ$U#nQ$a$OR$p$bQ#e!^Q#n!oQ$Q#lQ$R#mQ$l$[Q$w$kQ$}$uR%Q%PQ#|#eQ$T#nQ$^#}Q$`$OQ$j$US$o$a$bR$y$p!QUSTX]hikstw|!O!P!Q!R!S!T!h!z#Q#_#`#q$tqVOf}!^!_!o#d#k#l#m#w$P$[$k$u%PT$c$Q$dQ$g$QR$s$du_O]f}![!^!_!o#d#k#l#m#w$P$[$k$u%Pp[Of}!^!_!o#d#k#l#m#w$P$[$k$u%PQ!W]Q!rkQ#X!SR#[!T]zTX|!h#_#`qbOf}!^!_!o#d#k#l#m#w$P$[$k$u%PQfOR!afQpPR!upQsSR!wsQ!cgR#g!cQ!fgQ#h!cT#j!f#hS#w#d$PR$Y#wQ!}uQ#o!xT#s!}#oQ#QwQ#q!zT#t#Q#qQ$d$QR$q$dY|TX!h#_#`R#R|S!]`!YR#b!]TeOfScOfQ#S}`#c!^!o#l#m$[$k$u%PQ#f!_U#v#d#w$PR$O#kp`Of}!^!_!o#d#k#l#m#w$P$[$k$u%PQ!Y]R#a![Q!lhR$|$trYO]f}!^!_!o#d#k#l#m#w$P$[$k$u%PQwS[yTX|!h#_#`S!ih$tQ!miQ!pkQ!zsQ!{tW#Pw!z#Q#qQ#T!OQ#U!PQ#W!QQ#X!RQ#Y!SR#Z!TpWOf}!^!_!o#d#k#l#m#w$P$[$k$u%P!OxSTXhikstw|!O!P!Q!R!S!T!h!z#Q#_#`#q$tR!U]ToPpQ#x#dR$b$P]{TX|!h#_#`",
|
||||||
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo PlusEq MinusEq StarEq SlashEq ModuloEq 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 Identifier AssignableIdentifier Word IdentifierBeforeDot CurlyString Do Comment Program PipeExpr FunctionCall DotGet Number ParenExpr IfExpr keyword ConditionalOp String StringFragment Interpolation EscapeSeq DoubleQuote Boolean Regex Dict NamedArg NamedArgPrefix FunctionDef Params NamedParam Null colon CatchExpr keyword Block FinallyExpr keyword keyword Underscore Array ElseIfExpr keyword ElseExpr FunctionCallOrIdentifier BinOp PositionalArg operator WhileExpr keyword FunctionCallWithBlock TryExpr keyword Throw keyword CompoundAssign Assign",
|
||||||
maxTerm: 121,
|
maxTerm: 111,
|
||||||
context: trackScope,
|
context: trackScope,
|
||||||
nodeProps: [
|
nodeProps: [
|
||||||
["closedBy", 57,"end"]
|
["closedBy", 50,"end"]
|
||||||
],
|
],
|
||||||
propSources: [highlighting],
|
propSources: [highlighting],
|
||||||
skippedNodes: [0,34],
|
skippedNodes: [0,26],
|
||||||
repeatNodeCount: 12,
|
repeatNodeCount: 11,
|
||||||
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: "FV~R}OX$OXY$mYZ%WZp$Opq$mqr$Ors%qst'wtu)}uw$Owx*Sxy*Xyz*rz{$O{|+]|}$O}!O.P!O!P$O!P!Q0f!Q![+z![!]9R!]!^%W!^!}$O!}#O9l#O#P;b#P#Q;g#Q#R$O#R#S<Q#S#T$O#T#Y/Q#Y#Z<k#Z#b/Q#b#cAi#c#f/Q#f#gBf#g#h/Q#h#iCc#i#o/Q#o#p$O#p#qEg#q;'S$O;'S;=`$g<%l~$O~O$O~~FQS$TUuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OS$jP;=`<%l$O^$tUuS!zYOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU%_UuS#^QOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU%vZuSOr%qrs&ist%qtu'Suw%qwx'Sx#O%q#O#P'S#P;'S%q;'S;=`'q<%lO%qU&pUxQuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OQ'VTOr'Srs'fs;'S'S;'S;=`'k<%lO'SQ'kOxQQ'nP;=`<%l'SU'tP;=`<%l%q^'|WuSOp$Opq(fqt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O^(mZjYuSOY(fYZ$OZt(ftu)`uw(fwx)`x#O(f#O#P)`#P;'S(f;'S;=`)w<%lO(fY)eSjYOY)`Z;'S)`;'S;=`)q<%lO)`Y)tP;=`<%l)`^)zP;=`<%l(f~*SO#V~~*XO#T~U*`UuS#PQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU*yUuS#aQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU+bWuSOt$Ouw$Ox!Q$O!Q![+z![#O$O#P;'S$O;'S;=`$g<%lO$OU,RYuSoQOt$Ouw$Ox!O$O!O!P,q!P!Q$O!Q![+z![#O$O#P;'S$O;'S;=`$g<%lO$OU,vWuSOt$Ouw$Ox!Q$O!Q![-`![#O$O#P;'S$O;'S;=`$g<%lO$OU-gWuSoQOt$Ouw$Ox!Q$O!Q![-`![#O$O#P;'S$O;'S;=`$g<%lO$OU.U^uSOt$Ouw$Ox}$O}!O/Q!O!Q$O!Q![+z![!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$OU/V[uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$OU0SU}QuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU0kWuSOt$Ouw$Ox!P$O!P!Q1T!Q#O$O#P;'S$O;'S;=`$g<%lO$OU1Y^uSOY2UYZ$OZt2Utu3Xuw2Uwx3Xx!P2U!P!Q$O!Q!}2U!}#O7z#O#P5g#P;'S2U;'S;=`8{<%lO2UU2]^uSzQOY2UYZ$OZt2Utu3Xuw2Uwx3Xx!P2U!P!Q5|!Q!}2U!}#O7z#O#P5g#P;'S2U;'S;=`8{<%lO2UQ3^XzQOY3XZ!P3X!P!Q3y!Q!}3X!}#O4h#O#P5g#P;'S3X;'S;=`5v<%lO3XQ3|P!P!Q4PQ4UUzQ#Z#[4P#]#^4P#a#b4P#g#h4P#i#j4P#m#n4PQ4kVOY4hZ#O4h#O#P5Q#P#Q3X#Q;'S4h;'S;=`5a<%lO4hQ5TSOY4hZ;'S4h;'S;=`5a<%lO4hQ5dP;=`<%l4hQ5jSOY3XZ;'S3X;'S;=`5v<%lO3XQ5yP;=`<%l3XU6RWuSOt$Ouw$Ox!P$O!P!Q6k!Q#O$O#P;'S$O;'S;=`$g<%lO$OU6rbuSzQOt$Ouw$Ox#O$O#P#Z$O#Z#[6k#[#]$O#]#^6k#^#a$O#a#b6k#b#g$O#g#h6k#h#i$O#i#j6k#j#m$O#m#n6k#n;'S$O;'S;=`$g<%lO$OU8P[uSOY7zYZ$OZt7ztu4huw7zwx4hx#O7z#O#P5Q#P#Q2U#Q;'S7z;'S;=`8u<%lO7zU8xP;=`<%l7zU9OP;=`<%l2UU9YUuS!SQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU9sW#]QuSOt$Ouw$Ox!_$O!_!`:]!`#O$O#P;'S$O;'S;=`$g<%lO$OU:bVuSOt$Ouw$Ox#O$O#P#Q:w#Q;'S$O;'S;=`$g<%lO$OU;OU#[QuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O~;gO#W~U;nU#`QuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU<XUuS!ZQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU<p]uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#U=i#U#o/Q#o;'S$O;'S;=`$g<%lO$OU=n^uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#`/Q#`#a>j#a#o/Q#o;'S$O;'S;=`$g<%lO$OU>o^uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#g/Q#g#h?k#h#o/Q#o;'S$O;'S;=`$g<%lO$OU?p^uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#X/Q#X#Y@l#Y#o/Q#o;'S$O;'S;=`$g<%lO$OU@s[yQuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$O^Ap[#XWuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$O^Bm[#ZWuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$O^Cj^#YWuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#f/Q#f#gDf#g#o/Q#o;'S$O;'S;=`$g<%lO$OUDk^uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#i/Q#i#j?k#j#o/Q#o;'S$O;'S;=`$g<%lO$OUEnU!cQuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O~FVO#c~",
|
||||||
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#]~~", 11)],
|
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#O~~", 11)],
|
||||||
topRules: {"Program":[0,35]},
|
topRules: {"Program":[0,27]},
|
||||||
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: 20, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 20, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
|
||||||
tokenPrec: 2256
|
tokenPrec: 1682
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -368,118 +368,6 @@ describe('Parentheses', () => {
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Number 3`)
|
Number 3`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('function call with named args on multiple lines in parens', () => {
|
|
||||||
expect(`(tail
|
|
||||||
arg1=true
|
|
||||||
arg2=30
|
|
||||||
)`).toMatchTree(`
|
|
||||||
ParenExpr
|
|
||||||
FunctionCall
|
|
||||||
Identifier tail
|
|
||||||
NamedArg
|
|
||||||
NamedArgPrefix arg1=
|
|
||||||
Boolean true
|
|
||||||
NamedArg
|
|
||||||
NamedArgPrefix arg2=
|
|
||||||
Number 30
|
|
||||||
`)
|
|
||||||
|
|
||||||
expect(`(
|
|
||||||
tail
|
|
||||||
arg1=true
|
|
||||||
arg2=30
|
|
||||||
)`).toMatchTree(`
|
|
||||||
ParenExpr
|
|
||||||
FunctionCall
|
|
||||||
Identifier tail
|
|
||||||
NamedArg
|
|
||||||
NamedArgPrefix arg1=
|
|
||||||
Boolean true
|
|
||||||
NamedArg
|
|
||||||
NamedArgPrefix arg2=
|
|
||||||
Number 30
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('binop with newlines in parens', () => {
|
|
||||||
expect(`(
|
|
||||||
1 + 2
|
|
||||||
)`).toMatchTree(`
|
|
||||||
ParenExpr
|
|
||||||
BinOp
|
|
||||||
Number 1
|
|
||||||
Plus +
|
|
||||||
Number 2`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('comparison with newlines in parens', () => {
|
|
||||||
expect(`(
|
|
||||||
1 < 2
|
|
||||||
)`).toMatchTree(`
|
|
||||||
ParenExpr
|
|
||||||
ConditionalOp
|
|
||||||
Number 1
|
|
||||||
Lt <
|
|
||||||
Number 2`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('function call with multiple identifiers on separate lines in parens', () => {
|
|
||||||
expect(`(echo
|
|
||||||
arg1
|
|
||||||
arg2
|
|
||||||
arg3
|
|
||||||
)`).toMatchTree(`
|
|
||||||
ParenExpr
|
|
||||||
FunctionCall
|
|
||||||
Identifier echo
|
|
||||||
PositionalArg
|
|
||||||
Identifier arg1
|
|
||||||
PositionalArg
|
|
||||||
Identifier arg2
|
|
||||||
PositionalArg
|
|
||||||
Identifier arg3`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Number literals', () => {
|
|
||||||
test('allows underscores in integer literals', () => {
|
|
||||||
expect('10_000').toMatchTree(`Number 10_000`)
|
|
||||||
expect('1_000_000').toMatchTree(`Number 1_000_000`)
|
|
||||||
expect('100_000').toMatchTree(`Number 100_000`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('allows underscores in decimal literals', () => {
|
|
||||||
expect('3.14_159').toMatchTree(`Number 3.14_159`)
|
|
||||||
expect('1_000.50').toMatchTree(`Number 1_000.50`)
|
|
||||||
expect('0.000_001').toMatchTree(`Number 0.000_001`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('allows underscores in negative numbers', () => {
|
|
||||||
expect('-10_000').toMatchTree(`Number -10_000`)
|
|
||||||
expect('-3.14_159').toMatchTree(`Number -3.14_159`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('allows underscores in positive numbers with explicit sign', () => {
|
|
||||||
expect('+10_000').toMatchTree(`Number +10_000`)
|
|
||||||
expect('+3.14_159').toMatchTree(`Number +3.14_159`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('works in expressions', () => {
|
|
||||||
expect('1_000 + 2_000').toMatchTree(`
|
|
||||||
BinOp
|
|
||||||
Number 1_000
|
|
||||||
Plus +
|
|
||||||
Number 2_000`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('works in function calls', () => {
|
|
||||||
expect('echo 10_000').toMatchTree(`
|
|
||||||
FunctionCall
|
|
||||||
Identifier echo
|
|
||||||
PositionalArg
|
|
||||||
Number 10_000`)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('BinOp', () => {
|
describe('BinOp', () => {
|
||||||
|
|
@ -707,87 +595,6 @@ describe('CompoundAssign', () => {
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Number 3`)
|
Number 3`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('parses ??= operator', () => {
|
|
||||||
expect('x ??= 5').toMatchTree(`
|
|
||||||
CompoundAssign
|
|
||||||
AssignableIdentifier x
|
|
||||||
NullishEq ??=
|
|
||||||
Number 5`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses ??= with expression', () => {
|
|
||||||
expect('config ??= get-default').toMatchTree(`
|
|
||||||
CompoundAssign
|
|
||||||
AssignableIdentifier config
|
|
||||||
NullishEq ??=
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier get-default`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Nullish coalescing operator', () => {
|
|
||||||
test('? can still end an identifier', () => {
|
|
||||||
expect('what?').toMatchTree(`
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier what?`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('?? can still end an identifier', () => {
|
|
||||||
expect('what??').toMatchTree(`
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier what??`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('?? can still be in a word', () => {
|
|
||||||
expect('what??the').toMatchTree(`
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier what??the`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('?? can still start a word', () => {
|
|
||||||
expect('??what??the').toMatchTree(`
|
|
||||||
Word ??what??the`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses ?? operator', () => {
|
|
||||||
expect('x ?? 5').toMatchTree(`
|
|
||||||
ConditionalOp
|
|
||||||
Identifier x
|
|
||||||
NullishCoalesce ??
|
|
||||||
Number 5`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses chained ?? operators', () => {
|
|
||||||
expect('a ?? b ?? c').toMatchTree(`
|
|
||||||
ConditionalOp
|
|
||||||
ConditionalOp
|
|
||||||
Identifier a
|
|
||||||
NullishCoalesce ??
|
|
||||||
Identifier b
|
|
||||||
NullishCoalesce ??
|
|
||||||
Identifier c`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses ?? with expressions', () => {
|
|
||||||
expect('get-value ?? default-value').toMatchTree(`
|
|
||||||
ConditionalOp
|
|
||||||
Identifier get-value
|
|
||||||
NullishCoalesce ??
|
|
||||||
Identifier default-value`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses ?? with parenthesized function call', () => {
|
|
||||||
expect('get-value ?? (default 10)').toMatchTree(`
|
|
||||||
ConditionalOp
|
|
||||||
Identifier get-value
|
|
||||||
NullishCoalesce ??
|
|
||||||
ParenExpr
|
|
||||||
FunctionCall
|
|
||||||
Identifier default
|
|
||||||
PositionalArg
|
|
||||||
Number 10`)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('DotGet whitespace sensitivity', () => {
|
describe('DotGet whitespace sensitivity', () => {
|
||||||
|
|
@ -863,11 +670,11 @@ basename = 5 # very astute
|
||||||
})
|
})
|
||||||
|
|
||||||
test('words with # are not considered comments', () => {
|
test('words with # are not considered comments', () => {
|
||||||
expect('find my#hashtag-file.txt').toMatchTree(`
|
expect('find #hashtag-file.txt').toMatchTree(`
|
||||||
FunctionCall
|
FunctionCall
|
||||||
Identifier find
|
Identifier find
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Word my#hashtag-file.txt`)
|
Word #hashtag-file.txt`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('hastags in strings are not comments', () => {
|
test('hastags in strings are not comments', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
import { expect, describe, test } from 'bun:test'
|
|
||||||
|
|
||||||
import '../shrimp.grammar' // Importing this so changes cause it to retest!
|
|
||||||
|
|
||||||
describe('bitwise operators - grammar', () => {
|
|
||||||
test('parses band (bitwise AND)', () => {
|
|
||||||
expect('5 band 3').toMatchTree(`
|
|
||||||
BinOp
|
|
||||||
Number 5
|
|
||||||
Band band
|
|
||||||
Number 3`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses bor (bitwise OR)', () => {
|
|
||||||
expect('5 bor 3').toMatchTree(`
|
|
||||||
BinOp
|
|
||||||
Number 5
|
|
||||||
Bor bor
|
|
||||||
Number 3`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses bxor (bitwise XOR)', () => {
|
|
||||||
expect('5 bxor 3').toMatchTree(`
|
|
||||||
BinOp
|
|
||||||
Number 5
|
|
||||||
Bxor bxor
|
|
||||||
Number 3`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses << (left shift)', () => {
|
|
||||||
expect('5 << 2').toMatchTree(`
|
|
||||||
BinOp
|
|
||||||
Number 5
|
|
||||||
Shl <<
|
|
||||||
Number 2`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses >> (signed right shift)', () => {
|
|
||||||
expect('20 >> 2').toMatchTree(`
|
|
||||||
BinOp
|
|
||||||
Number 20
|
|
||||||
Shr >>
|
|
||||||
Number 2`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses >>> (unsigned right shift)', () => {
|
|
||||||
expect('-1 >>> 1').toMatchTree(`
|
|
||||||
BinOp
|
|
||||||
Number -1
|
|
||||||
Ushr >>>
|
|
||||||
Number 1`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses bnot (bitwise NOT) as function call', () => {
|
|
||||||
expect('bnot 5').toMatchTree(`
|
|
||||||
FunctionCall
|
|
||||||
Identifier bnot
|
|
||||||
PositionalArg
|
|
||||||
Number 5`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('bitwise operators work in expressions', () => {
|
|
||||||
expect('x = 5 band 3').toMatchTree(`
|
|
||||||
Assign
|
|
||||||
AssignableIdentifier x
|
|
||||||
Eq =
|
|
||||||
BinOp
|
|
||||||
Number 5
|
|
||||||
Band band
|
|
||||||
Number 3`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -2,65 +2,6 @@ import { expect, describe, test } from 'bun:test'
|
||||||
|
|
||||||
import '../shrimp.grammar' // Importing this so changes cause it to retest!
|
import '../shrimp.grammar' // Importing this so changes cause it to retest!
|
||||||
|
|
||||||
describe('number literals', () => {
|
|
||||||
test('binary numbers', () => {
|
|
||||||
expect('0b110').toMatchTree(`
|
|
||||||
Number 0b110
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('hex numbers', () => {
|
|
||||||
expect('0xdeadbeef').toMatchTree(`
|
|
||||||
Number 0xdeadbeef
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('hex numbers uppercase', () => {
|
|
||||||
expect('0xFF').toMatchTree(`
|
|
||||||
Number 0xFF
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('decimal numbers still work', () => {
|
|
||||||
expect('42').toMatchTree(`
|
|
||||||
Number 42
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('negative binary', () => {
|
|
||||||
expect('-0b110').toMatchTree(`
|
|
||||||
Number -0b110
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('negative hex', () => {
|
|
||||||
expect('-0xFF').toMatchTree(`
|
|
||||||
Number -0xFF
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('positive prefix binary', () => {
|
|
||||||
expect('+0b110').toMatchTree(`
|
|
||||||
Number +0b110
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('positive prefix hex', () => {
|
|
||||||
expect('+0xFF').toMatchTree(`
|
|
||||||
Number +0xFF
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('hex and binary in arrays', () => {
|
|
||||||
expect('[0xFF 0b110 42]').toMatchTree(`
|
|
||||||
Array
|
|
||||||
Number 0xFF
|
|
||||||
Number 0b110
|
|
||||||
Number 42
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('array literals', () => {
|
describe('array literals', () => {
|
||||||
test('work with numbers', () => {
|
test('work with numbers', () => {
|
||||||
expect('[1 2 3]').toMatchTree(`
|
expect('[1 2 3]').toMatchTree(`
|
||||||
|
|
|
||||||
|
|
@ -98,81 +98,4 @@ describe('pipe expressions', () => {
|
||||||
Identifier double
|
Identifier double
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('string literals can be piped', () => {
|
|
||||||
expect(`'hey there' | echo`).toMatchTree(`
|
|
||||||
PipeExpr
|
|
||||||
String
|
|
||||||
StringFragment hey there
|
|
||||||
operator |
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier echo
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('number literals can be piped', () => {
|
|
||||||
expect(`42 | echo`).toMatchTree(`
|
|
||||||
PipeExpr
|
|
||||||
Number 42
|
|
||||||
operator |
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier echo`)
|
|
||||||
|
|
||||||
expect(`4.22 | echo`).toMatchTree(`
|
|
||||||
PipeExpr
|
|
||||||
Number 4.22
|
|
||||||
operator |
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier echo`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('null literals can be piped', () => {
|
|
||||||
expect(`null | echo`).toMatchTree(`
|
|
||||||
PipeExpr
|
|
||||||
Null null
|
|
||||||
operator |
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier echo`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('boolean literals can be piped', () => {
|
|
||||||
expect(`true | echo`).toMatchTree(`
|
|
||||||
PipeExpr
|
|
||||||
Boolean true
|
|
||||||
operator |
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier echo`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('array literals can be piped', () => {
|
|
||||||
expect(`[1 2 3] | echo`).toMatchTree(`
|
|
||||||
PipeExpr
|
|
||||||
Array
|
|
||||||
Number 1
|
|
||||||
Number 2
|
|
||||||
Number 3
|
|
||||||
operator |
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier echo
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('dict literals can be piped', () => {
|
|
||||||
expect(`[a=1 b=2 c=3] | echo`).toMatchTree(`
|
|
||||||
PipeExpr
|
|
||||||
Dict
|
|
||||||
NamedArg
|
|
||||||
NamedArgPrefix a=
|
|
||||||
Number 1
|
|
||||||
NamedArg
|
|
||||||
NamedArgPrefix b=
|
|
||||||
Number 2
|
|
||||||
NamedArg
|
|
||||||
NamedArgPrefix c=
|
|
||||||
Number 3
|
|
||||||
operator |
|
|
||||||
FunctionCallOrIdentifier
|
|
||||||
Identifier echo
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ export function specializeKeyword(ident: string) {
|
||||||
|
|
||||||
// tell the dotGet searcher about builtin globals
|
// tell the dotGet searcher about builtin globals
|
||||||
export const globals: string[] = []
|
export const globals: string[] = []
|
||||||
export const setGlobals = (newGlobals: string[] | Record<string, any>) => {
|
export const setGlobals = (newGlobals: string[]) => {
|
||||||
globals.length = 0
|
globals.length = 0
|
||||||
globals.push(...(Array.isArray(newGlobals) ? newGlobals : Object.keys(newGlobals)))
|
globals.push(...newGlobals)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The only chars that can't be words are whitespace, apostrophes, closing parens, and EOF.
|
// The only chars that can't be words are whitespace, apostrophes, closing parens, and EOF.
|
||||||
|
|
@ -217,15 +217,6 @@ const chooseIdentifierToken = (input: InputStream, stack: Stack): number => {
|
||||||
|
|
||||||
const nextCh = getFullCodePoint(input, peekPos)
|
const nextCh = getFullCodePoint(input, peekPos)
|
||||||
const nextCh2 = getFullCodePoint(input, peekPos + 1)
|
const nextCh2 = getFullCodePoint(input, peekPos + 1)
|
||||||
const nextCh3 = getFullCodePoint(input, peekPos + 2)
|
|
||||||
|
|
||||||
// Check for ??= (three-character compound operator)
|
|
||||||
if (nextCh === 63 /* ? */ && nextCh2 === 63 /* ? */ && nextCh3 === 61 /* = */) {
|
|
||||||
const charAfterOp = getFullCodePoint(input, peekPos + 3)
|
|
||||||
if (isWhiteSpace(charAfterOp) || charAfterOp === -1 /* EOF */) {
|
|
||||||
return AssignableIdentifier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for compound assignment operators: +=, -=, *=, /=, %=
|
// Check for compound assignment operators: +=, -=, *=, /=, %=
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {
|
||||||
} from 'reefvm'
|
} from 'reefvm'
|
||||||
|
|
||||||
import { dict } from './dict'
|
import { dict } from './dict'
|
||||||
import { json } from './json'
|
|
||||||
import { load } from './load'
|
import { load } from './load'
|
||||||
import { list } from './list'
|
import { list } from './list'
|
||||||
import { math } from './math'
|
import { math } from './math'
|
||||||
|
|
@ -14,7 +13,6 @@ import { str } from './str'
|
||||||
|
|
||||||
export const globals = {
|
export const globals = {
|
||||||
dict,
|
dict,
|
||||||
json,
|
|
||||||
load,
|
load,
|
||||||
list,
|
list,
|
||||||
math,
|
math,
|
||||||
|
|
@ -42,11 +40,6 @@ export const globals = {
|
||||||
'var?': function (this: VM, v: string) {
|
'var?': function (this: VM, v: string) {
|
||||||
return typeof v !== 'string' || this.scope.has(v)
|
return typeof v !== 'string' || this.scope.has(v)
|
||||||
},
|
},
|
||||||
ref: (fn: Function) => fn,
|
|
||||||
|
|
||||||
// env
|
|
||||||
args: Bun.argv.slice(1),
|
|
||||||
exit: (num: number) => process.exit(num ?? 0),
|
|
||||||
|
|
||||||
// type predicates
|
// type predicates
|
||||||
'string?': (v: any) => toValue(v).type === 'string',
|
'string?': (v: any) => toValue(v).type === 'string',
|
||||||
|
|
@ -63,7 +56,6 @@ export const globals = {
|
||||||
|
|
||||||
// boolean/logic
|
// boolean/logic
|
||||||
not: (v: any) => !v,
|
not: (v: any) => !v,
|
||||||
bnot: (n: number) => ~(n | 0),
|
|
||||||
|
|
||||||
// utilities
|
// utilities
|
||||||
inc: (n: number) => n + 1,
|
inc: (n: number) => n + 1,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
export const json = {
|
|
||||||
encode: (s: any) => JSON.stringify(s),
|
|
||||||
decode: (s: string) => JSON.parse(s),
|
|
||||||
}
|
|
||||||
|
|
||||||
; (json as any).parse = json.decode
|
|
||||||
; (json as any).stringify = json.encode
|
|
||||||
|
|
@ -14,13 +14,6 @@ export const list = {
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
reject: async (list: any[], cb: Function) => {
|
|
||||||
let acc: any[] = []
|
|
||||||
for (const value of list) {
|
|
||||||
if (!(await cb(value))) acc.push(value)
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
reduce: async (list: any[], cb: Function, initial: any) => {
|
reduce: async (list: any[], cb: Function, initial: any) => {
|
||||||
let acc = initial
|
let acc = initial
|
||||||
for (const value of list) acc = await cb(acc, value)
|
for (const value of list) acc = await cb(acc, value)
|
||||||
|
|
@ -73,13 +66,6 @@ export const list = {
|
||||||
const realItems = items.map(item => item.value)
|
const realItems = items.map(item => item.value)
|
||||||
return toValue(realList.splice(realStart, realDeleteCount, ...realItems))
|
return toValue(realList.splice(realStart, realDeleteCount, ...realItems))
|
||||||
},
|
},
|
||||||
insert: (list: Value, index: Value, item: Value) => {
|
|
||||||
if (list.type !== 'array') return toNull()
|
|
||||||
const realList = list.value as any[]
|
|
||||||
const realIndex = index.value as number
|
|
||||||
realList.splice(realIndex, 0, item)
|
|
||||||
return toValue(realList.length)
|
|
||||||
},
|
|
||||||
|
|
||||||
// sequence operations
|
// sequence operations
|
||||||
reverse: (list: any[]) => list.slice().reverse(),
|
reverse: (list: any[]) => list.slice().reverse(),
|
||||||
|
|
@ -150,4 +136,3 @@ export const list = {
|
||||||
; (list.pop as any).raw = true
|
; (list.pop as any).raw = true
|
||||||
; (list.shift as any).raw = true
|
; (list.shift as any).raw = true
|
||||||
; (list.unshift as any).raw = true
|
; (list.unshift as any).raw = true
|
||||||
; (list.insert as any).raw = true
|
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
// strings
|
// strings
|
||||||
export const str = {
|
export const str = {
|
||||||
join: (arr: string[], sep: string = ',') => arr.join(sep),
|
join: (arr: string[], sep: string = ',') => arr.join(sep),
|
||||||
split: (str: string, sep: string = ',') => String(str ?? '').split(sep),
|
split: (str: string, sep: string = ',') => str.split(sep),
|
||||||
'to-upper': (str: string) => String(str ?? '').toUpperCase(),
|
'to-upper': (str: string) => str.toUpperCase(),
|
||||||
'to-lower': (str: string) => String(str ?? '').toLowerCase(),
|
'to-lower': (str: string) => str.toLowerCase(),
|
||||||
trim: (str: string) => String(str ?? '').trim(),
|
trim: (str: string) => str.trim(),
|
||||||
|
|
||||||
// predicates
|
// predicates
|
||||||
'starts-with?': (str: string, prefix: string) => String(str ?? '').startsWith(prefix),
|
'starts-with?': (str: string, prefix: string) => str.startsWith(prefix),
|
||||||
'ends-with?': (str: string, suffix: string) => String(str ?? '').endsWith(suffix),
|
'ends-with?': (str: string, suffix: string) => str.endsWith(suffix),
|
||||||
'contains?': (str: string, substr: string) => String(str ?? '').includes(substr),
|
'contains?': (str: string, substr: string) => str.includes(substr),
|
||||||
'empty?': (str: string) => String(str ?? '').length === 0,
|
'empty?': (str: string) => str.length === 0,
|
||||||
|
|
||||||
// inspection
|
// inspection
|
||||||
'index-of': (str: string, search: string) => String(str ?? '').indexOf(search),
|
'index-of': (str: string, search: string) => str.indexOf(search),
|
||||||
'last-index-of': (str: string, search: string) => String(str ?? '').lastIndexOf(search),
|
'last-index-of': (str: string, search: string) => str.lastIndexOf(search),
|
||||||
|
|
||||||
// transformations
|
// transformations
|
||||||
replace: (str: string, search: string, replacement: string) => String(str ?? '').replace(search, replacement),
|
replace: (str: string, search: string, replacement: string) => str.replace(search, replacement),
|
||||||
'replace-all': (str: string, search: string, replacement: string) => String(str ?? '').replaceAll(search, replacement),
|
'replace-all': (str: string, search: string, replacement: string) => str.replaceAll(search, replacement),
|
||||||
slice: (str: string, start: number, end?: number | null) => String(str ?? '').slice(start, end ?? undefined),
|
slice: (str: string, start: number, end?: number | null) => str.slice(start, end ?? undefined),
|
||||||
substring: (str: string, start: number, end?: number | null) => String(str ?? '').substring(start, end ?? undefined),
|
substring: (str: string, start: number, end?: number | null) => str.substring(start, end ?? undefined),
|
||||||
repeat: (str: string, count: number) => {
|
repeat: (str: string, count: number) => {
|
||||||
if (count < 0) throw new Error(`repeat: count must be non-negative, got ${count}`)
|
if (count < 0) throw new Error(`repeat: count must be non-negative, got ${count}`)
|
||||||
if (!Number.isInteger(count)) throw new Error(`repeat: count must be an integer, got ${count}`)
|
if (!Number.isInteger(count)) throw new Error(`repeat: count must be an integer, got ${count}`)
|
||||||
return String(str ?? '').repeat(count)
|
return str.repeat(count)
|
||||||
},
|
},
|
||||||
'pad-start': (str: string, length: number, pad: string = ' ') => String(str ?? '').padStart(length, pad),
|
'pad-start': (str: string, length: number, pad: string = ' ') => str.padStart(length, pad),
|
||||||
'pad-end': (str: string, length: number, pad: string = ' ') => String(str ?? '').padEnd(length, pad),
|
'pad-end': (str: string, length: number, pad: string = ' ') => str.padEnd(length, pad),
|
||||||
lines: (str: string) => String(str ?? '').split('\n'),
|
lines: (str: string) => str.split('\n'),
|
||||||
chars: (str: string) => String(str ?? '').split(''),
|
chars: (str: string) => str.split(''),
|
||||||
|
|
||||||
// regex
|
// regex
|
||||||
match: (str: string, regex: RegExp) => String(str ?? '').match(regex),
|
match: (str: string, regex: RegExp) => str.match(regex),
|
||||||
'test?': (str: string, regex: RegExp) => regex.test(String(str ?? '')),
|
'test?': (str: string, regex: RegExp) => regex.test(str),
|
||||||
}
|
}
|
||||||
|
|
@ -77,29 +77,3 @@ describe('introspection', () => {
|
||||||
await expect(`describe 'hello'`).toEvaluateTo("#<string: \u001b[32m'hello\u001b[32m'\u001b[0m>", globals)
|
await expect(`describe 'hello'`).toEvaluateTo("#<string: \u001b[32m'hello\u001b[32m'\u001b[0m>", globals)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('environment', () => {
|
|
||||||
test('args is an array', async () => {
|
|
||||||
await expect(`array? args`).toEvaluateTo(true, globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('args can be accessed', async () => {
|
|
||||||
await expect(`type args`).toEvaluateTo('array', globals)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('', async () => {
|
|
||||||
await expect(`list.first args | str.ends-with? 'shrimp.test.ts'`).toEvaluateTo(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('ref', () => {
|
|
||||||
expect(`rnd = do x: true end; rnd | type`).toEvaluateTo('boolean')
|
|
||||||
expect(`rnd = do x: true end; ref rnd | type`).toEvaluateTo('function')
|
|
||||||
|
|
||||||
expect(`math.random | type`).toEvaluateTo('number')
|
|
||||||
expect(`ref math.random | type`).toEvaluateTo('native')
|
|
||||||
|
|
||||||
expect(`rnd = math.random; rnd | type`).toEvaluateTo('number')
|
|
||||||
expect(`rnd = ref math.random; rnd | type`).toEvaluateTo('number')
|
|
||||||
expect(`rnd = ref math.random; ref rnd | type`).toEvaluateTo('native')
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
import { expect, describe, test } from 'bun:test'
|
|
||||||
|
|
||||||
describe('json', () => {
|
|
||||||
test('json.decode', () => {
|
|
||||||
expect(`json.decode '[1,2,3]'`).toEvaluateTo([1, 2, 3])
|
|
||||||
expect(`json.decode '"heya"'`).toEvaluateTo('heya')
|
|
||||||
expect(`json.decode '[true, false, null]'`).toEvaluateTo([true, false, null])
|
|
||||||
expect(`json.decode '{"a": true, "b": false, "c": "yeah"}'`).toEvaluateTo({ a: true, b: false, c: "yeah" })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('json.encode', () => {
|
|
||||||
expect(`json.encode [1 2 3]`).toEvaluateTo('[1,2,3]')
|
|
||||||
expect(`json.encode 'heya'`).toEvaluateTo('"heya"')
|
|
||||||
expect(`json.encode [true false null]`).toEvaluateTo('[true,false,null]')
|
|
||||||
expect(`json.encode [a=true b=false c='yeah'] | json.decode`).toEvaluateTo({ a: true, b: false, c: "yeah" })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('edge cases - empty structures', () => {
|
|
||||||
expect(`json.decode '[]'`).toEvaluateTo([])
|
|
||||||
expect(`json.decode '{}'`).toEvaluateTo({})
|
|
||||||
expect(`json.encode []`).toEvaluateTo('[]')
|
|
||||||
expect(`json.encode [=]`).toEvaluateTo('{}')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('edge cases - special characters in strings', () => {
|
|
||||||
expect(`json.decode '"hello\\\\nworld"'`).toEvaluateTo('hello\nworld')
|
|
||||||
expect(`json.decode '"tab\\\\there"'`).toEvaluateTo('tab\there')
|
|
||||||
expect(`json.decode '"forward/slash"'`).toEvaluateTo('forward/slash')
|
|
||||||
expect(`json.decode '"with\\\\\\\\backslash"'`).toEvaluateTo('with\\backslash')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('numbers - integers and floats', () => {
|
|
||||||
expect(`json.decode '42'`).toEvaluateTo(42)
|
|
||||||
expect(`json.decode '0'`).toEvaluateTo(0)
|
|
||||||
expect(`json.decode '-17'`).toEvaluateTo(-17)
|
|
||||||
expect(`json.decode '3.14159'`).toEvaluateTo(3.14159)
|
|
||||||
expect(`json.decode '-0.5'`).toEvaluateTo(-0.5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('numbers - scientific notation', () => {
|
|
||||||
expect(`json.decode '1e10'`).toEvaluateTo(1e10)
|
|
||||||
expect(`json.decode '2.5e-3'`).toEvaluateTo(2.5e-3)
|
|
||||||
expect(`json.decode '1.23E+5'`).toEvaluateTo(1.23e5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('unicode - emoji and special characters', () => {
|
|
||||||
expect(`json.decode '"hello 👋"'`).toEvaluateTo('hello 👋')
|
|
||||||
expect(`json.decode '"🎉🚀✨"'`).toEvaluateTo('🎉🚀✨')
|
|
||||||
expect(`json.encode '你好'`).toEvaluateTo('"你好"')
|
|
||||||
expect(`json.encode 'café'`).toEvaluateTo('"café"')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('nested structures - arrays', () => {
|
|
||||||
expect(`json.decode '[[1,2],[3,4],[5,6]]'`).toEvaluateTo([[1, 2], [3, 4], [5, 6]])
|
|
||||||
expect(`json.decode '[1,[2,[3,[4]]]]'`).toEvaluateTo([1, [2, [3, [4]]]])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('nested structures - objects', () => {
|
|
||||||
expect(`json.decode '{"user":{"name":"Alice","age":30}}'`).toEvaluateTo({
|
|
||||||
user: { name: 'Alice', age: 30 }
|
|
||||||
})
|
|
||||||
expect(`json.decode '{"a":{"b":{"c":"deep"}}}'`).toEvaluateTo({
|
|
||||||
a: { b: { c: 'deep' } }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('nested structures - mixed arrays and objects', () => {
|
|
||||||
expect(`json.decode '[{"id":1,"tags":["a","b"]},{"id":2,"tags":["c"]}]'`).toEvaluateTo([
|
|
||||||
{ id: 1, tags: ['a', 'b'] },
|
|
||||||
{ id: 2, tags: ['c'] }
|
|
||||||
])
|
|
||||||
expect(`json.decode '{"items":[1,2,3],"meta":{"count":3}}'`).toEvaluateTo({
|
|
||||||
items: [1, 2, 3],
|
|
||||||
meta: { count: 3 }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('error handling - invalid json', () => {
|
|
||||||
expect(`json.decode '{invalid}'`).toFailEvaluation()
|
|
||||||
expect(`json.decode '[1,2,3'`).toFailEvaluation()
|
|
||||||
expect(`json.decode 'undefined'`).toFailEvaluation()
|
|
||||||
expect(`json.decode ''`).toFailEvaluation()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -3,186 +3,185 @@ import { globals } from '#prelude'
|
||||||
|
|
||||||
describe('string operations', () => {
|
describe('string operations', () => {
|
||||||
test('to-upper converts to uppercase', async () => {
|
test('to-upper converts to uppercase', async () => {
|
||||||
await expect(`str.to-upper 'hello'`).toEvaluateTo('HELLO')
|
await expect(`str.to-upper 'hello'`).toEvaluateTo('HELLO', globals)
|
||||||
await expect(`str.to-upper 'Hello World!'`).toEvaluateTo('HELLO WORLD!')
|
await expect(`str.to-upper 'Hello World!'`).toEvaluateTo('HELLO WORLD!', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('to-lower converts to lowercase', async () => {
|
test('to-lower converts to lowercase', async () => {
|
||||||
await expect(`str.to-lower 'HELLO'`).toEvaluateTo('hello')
|
await expect(`str.to-lower 'HELLO'`).toEvaluateTo('hello', globals)
|
||||||
await expect(`str.to-lower 'Hello World!'`).toEvaluateTo('hello world!')
|
await expect(`str.to-lower 'Hello World!'`).toEvaluateTo('hello world!', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('trim removes whitespace', async () => {
|
test('trim removes whitespace', async () => {
|
||||||
await expect(`str.trim ' hello '`).toEvaluateTo('hello')
|
await expect(`str.trim ' hello '`).toEvaluateTo('hello', globals)
|
||||||
await expect(`str.trim '\\n\\thello\\t\\n'`).toEvaluateTo('hello')
|
await expect(`str.trim '\\n\\thello\\t\\n'`).toEvaluateTo('hello', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('split divides string by separator', async () => {
|
test('split divides string by separator', async () => {
|
||||||
await expect(`str.split 'a,b,c' ','`).toEvaluateTo(['a', 'b', 'c'])
|
await expect(`str.split 'a,b,c' ','`).toEvaluateTo(['a', 'b', 'c'], globals)
|
||||||
await expect(`str.split 'hello' ''`).toEvaluateTo(['h', 'e', 'l', 'l', 'o'])
|
await expect(`str.split 'hello' ''`).toEvaluateTo(['h', 'e', 'l', 'l', 'o'], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('split with comma separator', async () => {
|
test('split with comma separator', async () => {
|
||||||
await expect(`str.split 'a,b,c' ','`).toEvaluateTo(['a', 'b', 'c'])
|
await expect(`str.split 'a,b,c' ','`).toEvaluateTo(['a', 'b', 'c'], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('join combines array elements', async () => {
|
test('join combines array elements', async () => {
|
||||||
await expect(`str.join ['a' 'b' 'c'] '-'`).toEvaluateTo('a-b-c')
|
await expect(`str.join ['a' 'b' 'c'] '-'`).toEvaluateTo('a-b-c', globals)
|
||||||
await expect(`str.join ['hello' 'world'] ' '`).toEvaluateTo('hello world')
|
await expect(`str.join ['hello' 'world'] ' '`).toEvaluateTo('hello world', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('join with comma separator', async () => {
|
test('join with comma separator', async () => {
|
||||||
await expect(`str.join ['a' 'b' 'c'] ','`).toEvaluateTo('a,b,c')
|
await expect(`str.join ['a' 'b' 'c'] ','`).toEvaluateTo('a,b,c', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('starts-with? checks string prefix', async () => {
|
test('starts-with? checks string prefix', async () => {
|
||||||
await expect(`str.starts-with? 'hello' 'hel'`).toEvaluateTo(true)
|
await expect(`str.starts-with? 'hello' 'hel'`).toEvaluateTo(true, globals)
|
||||||
await expect(`str.starts-with? 'hello' 'bye'`).toEvaluateTo(false)
|
await expect(`str.starts-with? 'hello' 'bye'`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('ends-with? checks string suffix', async () => {
|
test('ends-with? checks string suffix', async () => {
|
||||||
await expect(`str.ends-with? 'hello' 'lo'`).toEvaluateTo(true)
|
await expect(`str.ends-with? 'hello' 'lo'`).toEvaluateTo(true, globals)
|
||||||
await expect(`str.ends-with? 'hello' 'he'`).toEvaluateTo(false)
|
await expect(`str.ends-with? 'hello' 'he'`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('contains? checks for substring', async () => {
|
test('contains? checks for substring', async () => {
|
||||||
await expect(`str.contains? 'hello world' 'o w'`).toEvaluateTo(true)
|
await expect(`str.contains? 'hello world' 'o w'`).toEvaluateTo(true, globals)
|
||||||
await expect(`str.contains? 'hello' 'bye'`).toEvaluateTo(false)
|
await expect(`str.contains? 'hello' 'bye'`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('empty? checks if string is empty', async () => {
|
test('empty? checks if string is empty', async () => {
|
||||||
await expect(`str.empty? ''`).toEvaluateTo(true)
|
await expect(`str.empty? ''`).toEvaluateTo(true, globals)
|
||||||
await expect(`str.empty? 'hello'`).toEvaluateTo(false)
|
await expect(`str.empty? 'hello'`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('replace replaces first occurrence', async () => {
|
test('replace replaces first occurrence', async () => {
|
||||||
await expect(`str.replace 'hello hello' 'hello' 'hi'`).toEvaluateTo('hi hello')
|
await expect(`str.replace 'hello hello' 'hello' 'hi'`).toEvaluateTo('hi hello', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('replace-all replaces all occurrences', async () => {
|
test('replace-all replaces all occurrences', async () => {
|
||||||
await expect(`str.replace-all 'hello hello' 'hello' 'hi'`).toEvaluateTo('hi hi')
|
await expect(`str.replace-all 'hello hello' 'hello' 'hi'`).toEvaluateTo('hi hi', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('slice extracts substring', async () => {
|
test('slice extracts substring', async () => {
|
||||||
await expect(`str.slice 'hello' 1 3`).toEvaluateTo('el')
|
await expect(`str.slice 'hello' 1 3`).toEvaluateTo('el', globals)
|
||||||
await expect(`str.slice 'hello' 2 null`).toEvaluateTo('llo')
|
await expect(`str.slice 'hello' 2 null`).toEvaluateTo('llo', globals)
|
||||||
await expect(`str.slice 'hello' 2`).toEvaluateTo('llo')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('repeat repeats string', async () => {
|
test('repeat repeats string', async () => {
|
||||||
await expect(`str.repeat 'ha' 3`).toEvaluateTo('hahaha')
|
await expect(`str.repeat 'ha' 3`).toEvaluateTo('hahaha', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('pad-start pads beginning', async () => {
|
test('pad-start pads beginning', async () => {
|
||||||
await expect(`str.pad-start '5' 3 '0'`).toEvaluateTo('005')
|
await expect(`str.pad-start '5' 3 '0'`).toEvaluateTo('005', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('pad-end pads end', async () => {
|
test('pad-end pads end', async () => {
|
||||||
await expect(`str.pad-end '5' 3 '0'`).toEvaluateTo('500')
|
await expect(`str.pad-end '5' 3 '0'`).toEvaluateTo('500', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('lines splits by newlines', async () => {
|
test('lines splits by newlines', async () => {
|
||||||
await expect(`str.lines 'a\\nb\\nc'`).toEvaluateTo(['a', 'b', 'c'])
|
await expect(`str.lines 'a\\nb\\nc'`).toEvaluateTo(['a', 'b', 'c'], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('chars splits into characters', async () => {
|
test('chars splits into characters', async () => {
|
||||||
await expect(`str.chars 'abc'`).toEvaluateTo(['a', 'b', 'c'])
|
await expect(`str.chars 'abc'`).toEvaluateTo(['a', 'b', 'c'], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('index-of finds substring position', async () => {
|
test('index-of finds substring position', async () => {
|
||||||
await expect(`str.index-of 'hello world' 'world'`).toEvaluateTo(6)
|
await expect(`str.index-of 'hello world' 'world'`).toEvaluateTo(6, globals)
|
||||||
await expect(`str.index-of 'hello' 'bye'`).toEvaluateTo(-1)
|
await expect(`str.index-of 'hello' 'bye'`).toEvaluateTo(-1, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('last-index-of finds last occurrence', async () => {
|
test('last-index-of finds last occurrence', async () => {
|
||||||
await expect(`str.last-index-of 'hello hello' 'hello'`).toEvaluateTo(6)
|
await expect(`str.last-index-of 'hello hello' 'hello'`).toEvaluateTo(6, globals)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('boolean logic', () => {
|
describe('boolean logic', () => {
|
||||||
test('not negates value', async () => {
|
test('not negates value', async () => {
|
||||||
await expect(`not true`).toEvaluateTo(false)
|
await expect(`not true`).toEvaluateTo(false, globals)
|
||||||
await expect(`not false`).toEvaluateTo(true)
|
await expect(`not false`).toEvaluateTo(true, globals)
|
||||||
await expect(`not 42`).toEvaluateTo(false)
|
await expect(`not 42`).toEvaluateTo(false, globals)
|
||||||
await expect(`not null`).toEvaluateTo(true)
|
await expect(`not null`).toEvaluateTo(true, globals)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('utilities', () => {
|
describe('utilities', () => {
|
||||||
test('inc increments by 1', async () => {
|
test('inc increments by 1', async () => {
|
||||||
await expect(`inc 5`).toEvaluateTo(6)
|
await expect(`inc 5`).toEvaluateTo(6, globals)
|
||||||
await expect(`inc -1`).toEvaluateTo(0)
|
await expect(`inc -1`).toEvaluateTo(0, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dec decrements by 1', async () => {
|
test('dec decrements by 1', async () => {
|
||||||
await expect(`dec 5`).toEvaluateTo(4)
|
await expect(`dec 5`).toEvaluateTo(4, globals)
|
||||||
await expect(`dec 0`).toEvaluateTo(-1)
|
await expect(`dec 0`).toEvaluateTo(-1, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('identity returns value as-is', async () => {
|
test('identity returns value as-is', async () => {
|
||||||
await expect(`identity 42`).toEvaluateTo(42)
|
await expect(`identity 42`).toEvaluateTo(42, globals)
|
||||||
await expect(`identity 'hello'`).toEvaluateTo('hello')
|
await expect(`identity 'hello'`).toEvaluateTo('hello', globals)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('collections', () => {
|
describe('collections', () => {
|
||||||
test('length', async () => {
|
test('length', async () => {
|
||||||
await expect(`length 'hello'`).toEvaluateTo(5)
|
await expect(`length 'hello'`).toEvaluateTo(5, globals)
|
||||||
await expect(`length [1 2 3]`).toEvaluateTo(3)
|
await expect(`length [1 2 3]`).toEvaluateTo(3, globals)
|
||||||
await expect(`length [a=1 b=2]`).toEvaluateTo(2)
|
await expect(`length [a=1 b=2]`).toEvaluateTo(2, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('length throws on invalid types', async () => {
|
test('length throws on invalid types', async () => {
|
||||||
await expect(`try: length 42 catch e: 'error' end`).toEvaluateTo('error')
|
await expect(`try: length 42 catch e: 'error' end`).toEvaluateTo('error', globals)
|
||||||
await expect(`try: length true catch e: 'error' end`).toEvaluateTo('error')
|
await expect(`try: length true catch e: 'error' end`).toEvaluateTo('error', globals)
|
||||||
await expect(`try: length null catch e: 'error' end`).toEvaluateTo('error')
|
await expect(`try: length null catch e: 'error' end`).toEvaluateTo('error', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('literal array creates array from arguments', async () => {
|
test('literal array creates array from arguments', async () => {
|
||||||
await expect(`[ 1 2 3 ]`).toEvaluateTo([1, 2, 3])
|
await expect(`[ 1 2 3 ]`).toEvaluateTo([1, 2, 3], globals)
|
||||||
await expect(`['a' 'b']`).toEvaluateTo(['a', 'b'])
|
await expect(`['a' 'b']`).toEvaluateTo(['a', 'b'], globals)
|
||||||
await expect(`[]`).toEvaluateTo([])
|
await expect(`[]`).toEvaluateTo([], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('literal dict creates object from named arguments', async () => {
|
test('literal dict creates object from named arguments', async () => {
|
||||||
await expect(`[ a=1 b=2 ]`).toEvaluateTo({ a: 1, b: 2 })
|
await expect(`[ a=1 b=2 ]`).toEvaluateTo({ a: 1, b: 2 }, globals)
|
||||||
await expect(`[=]`).toEvaluateTo({})
|
await expect(`[=]`).toEvaluateTo({}, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('at retrieves element at index', async () => {
|
test('at retrieves element at index', async () => {
|
||||||
await expect(`at [10 20 30] 0`).toEvaluateTo(10)
|
await expect(`at [10 20 30] 0`).toEvaluateTo(10, globals)
|
||||||
await expect(`at [10 20 30] 2`).toEvaluateTo(30)
|
await expect(`at [10 20 30] 2`).toEvaluateTo(30, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('at retrieves property from object', async () => {
|
test('at retrieves property from object', async () => {
|
||||||
await expect(`at [name='test'] 'name'`).toEvaluateTo('test')
|
await expect(`at [name='test'] 'name'`).toEvaluateTo('test', globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('slice extracts array subset', async () => {
|
test('slice extracts array subset', async () => {
|
||||||
await expect(`list.slice [1 2 3 4 5] 1 3`).toEvaluateTo([2, 3])
|
await expect(`list.slice [1 2 3 4 5] 1 3`).toEvaluateTo([2, 3], globals)
|
||||||
await expect(`list.slice [1 2 3 4 5] 2 5`).toEvaluateTo([3, 4, 5])
|
await expect(`list.slice [1 2 3 4 5] 2 5`).toEvaluateTo([3, 4, 5], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('range creates number sequence', async () => {
|
test('range creates number sequence', async () => {
|
||||||
await expect(`range 0 5`).toEvaluateTo([0, 1, 2, 3, 4, 5])
|
await expect(`range 0 5`).toEvaluateTo([0, 1, 2, 3, 4, 5], globals)
|
||||||
await expect(`range 3 6`).toEvaluateTo([3, 4, 5, 6])
|
await expect(`range 3 6`).toEvaluateTo([3, 4, 5, 6], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('range with single argument starts from 0', async () => {
|
test('range with single argument starts from 0', async () => {
|
||||||
await expect(`range 3 null`).toEvaluateTo([0, 1, 2, 3])
|
await expect(`range 3 null`).toEvaluateTo([0, 1, 2, 3], globals)
|
||||||
await expect(`range 0 null`).toEvaluateTo([0])
|
await expect(`range 0 null`).toEvaluateTo([0], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('empty? checks if list, dict, string is empty', async () => {
|
test('empty? checks if list, dict, string is empty', async () => {
|
||||||
await expect(`empty? []`).toEvaluateTo(true)
|
await expect(`empty? []`).toEvaluateTo(true, globals)
|
||||||
await expect(`empty? [1]`).toEvaluateTo(false)
|
await expect(`empty? [1]`).toEvaluateTo(false, globals)
|
||||||
|
|
||||||
await expect(`empty? [=]`).toEvaluateTo(true)
|
await expect(`empty? [=]`).toEvaluateTo(true, globals)
|
||||||
await expect(`empty? [a=true]`).toEvaluateTo(false)
|
await expect(`empty? [a=true]`).toEvaluateTo(false, globals)
|
||||||
|
|
||||||
await expect(`empty? ''`).toEvaluateTo(true)
|
await expect(`empty? ''`).toEvaluateTo(true, globals)
|
||||||
await expect(`empty? 'cat'`).toEvaluateTo(false)
|
await expect(`empty? 'cat'`).toEvaluateTo(false, globals)
|
||||||
await expect(`empty? meow`).toEvaluateTo(false)
|
await expect(`empty? meow`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.filter keeps matching elements', async () => {
|
test('list.filter keeps matching elements', async () => {
|
||||||
|
|
@ -191,16 +190,7 @@ describe('collections', () => {
|
||||||
x == 3 or x == 4 or x == 5
|
x == 3 or x == 4 or x == 5
|
||||||
end
|
end
|
||||||
list.filter [1 2 3 4 5] is-positive
|
list.filter [1 2 3 4 5] is-positive
|
||||||
`).toEvaluateTo([3, 4, 5])
|
`).toEvaluateTo([3, 4, 5], globals)
|
||||||
})
|
|
||||||
|
|
||||||
test('list.reject doesnt keep matching elements', async () => {
|
|
||||||
await expect(`
|
|
||||||
is-even = do x:
|
|
||||||
(x % 2) == 0
|
|
||||||
end
|
|
||||||
list.reject [1 2 3 4 5] is-even
|
|
||||||
`).toEvaluateTo([1, 3, 5])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.reduce accumulates values', async () => {
|
test('list.reduce accumulates values', async () => {
|
||||||
|
|
@ -209,7 +199,7 @@ describe('collections', () => {
|
||||||
acc + x
|
acc + x
|
||||||
end
|
end
|
||||||
list.reduce [1 2 3 4] add 0
|
list.reduce [1 2 3 4] add 0
|
||||||
`).toEvaluateTo(10)
|
`).toEvaluateTo(10, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.find returns first match', async () => {
|
test('list.find returns first match', async () => {
|
||||||
|
|
@ -218,155 +208,139 @@ describe('collections', () => {
|
||||||
x == 4
|
x == 4
|
||||||
end
|
end
|
||||||
list.find [1 2 4 5] is-four
|
list.find [1 2 4 5] is-four
|
||||||
`).toEvaluateTo(4)
|
`).toEvaluateTo(4, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.find returns null if no match', async () => {
|
test('list.find returns null if no match', async () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
is-ten = do x: x == 10 end
|
is-ten = do x: x == 10 end
|
||||||
list.find [1 2 3] is-ten
|
list.find [1 2 3] is-ten
|
||||||
`).toEvaluateTo(null)
|
`).toEvaluateTo(null, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.empty? checks if list is empty', async () => {
|
test('list.empty? checks if list is empty', async () => {
|
||||||
await expect(`list.empty? []`).toEvaluateTo(true)
|
await expect(`list.empty? []`).toEvaluateTo(true, globals)
|
||||||
await expect(`list.empty? [1]`).toEvaluateTo(false)
|
await expect(`list.empty? [1]`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.contains? checks for element', async () => {
|
test('list.contains? checks for element', async () => {
|
||||||
await expect(`list.contains? [1 2 3] 2`).toEvaluateTo(true)
|
await expect(`list.contains? [1 2 3] 2`).toEvaluateTo(true, globals)
|
||||||
await expect(`list.contains? [1 2 3] 5`).toEvaluateTo(false)
|
await expect(`list.contains? [1 2 3] 5`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.reverse reverses array', async () => {
|
test('list.reverse reverses array', async () => {
|
||||||
await expect(`list.reverse [1 2 3]`).toEvaluateTo([3, 2, 1])
|
await expect(`list.reverse [1 2 3]`).toEvaluateTo([3, 2, 1], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.concat combines arrays', async () => {
|
test('list.concat combines arrays', async () => {
|
||||||
await expect(`list.concat [1 2] [3 4]`).toEvaluateTo([1, 2, 3, 4])
|
await expect(`list.concat [1 2] [3 4]`).toEvaluateTo([1, 2, 3, 4], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.flatten flattens nested arrays', async () => {
|
test('list.flatten flattens nested arrays', async () => {
|
||||||
await expect(`list.flatten [[1 2] [3 4]] 1`).toEvaluateTo([1, 2, 3, 4])
|
await expect(`list.flatten [[1 2] [3 4]] 1`).toEvaluateTo([1, 2, 3, 4], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.unique removes duplicates', async () => {
|
test('list.unique removes duplicates', async () => {
|
||||||
await expect(`list.unique [1 2 2 3 1]`).toEvaluateTo([1, 2, 3])
|
await expect(`list.unique [1 2 2 3 1]`).toEvaluateTo([1, 2, 3], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.zip combines two arrays', async () => {
|
test('list.zip combines two arrays', async () => {
|
||||||
await expect(`list.zip [1 2] [3 4]`).toEvaluateTo([[1, 3], [2, 4]])
|
await expect(`list.zip [1 2] [3 4]`).toEvaluateTo([[1, 3], [2, 4]], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.first returns first element', async () => {
|
test('list.first returns first element', async () => {
|
||||||
await expect(`list.first [1 2 3]`).toEvaluateTo(1)
|
await expect(`list.first [1 2 3]`).toEvaluateTo(1, globals)
|
||||||
await expect(`list.first []`).toEvaluateTo(null)
|
await expect(`list.first []`).toEvaluateTo(null, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.last returns last element', async () => {
|
test('list.last returns last element', async () => {
|
||||||
await expect(`list.last [1 2 3]`).toEvaluateTo(3)
|
await expect(`list.last [1 2 3]`).toEvaluateTo(3, globals)
|
||||||
await expect(`list.last []`).toEvaluateTo(null)
|
await expect(`list.last []`).toEvaluateTo(null, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.rest returns all but first', async () => {
|
test('list.rest returns all but first', async () => {
|
||||||
await expect(`list.rest [1 2 3]`).toEvaluateTo([2, 3])
|
await expect(`list.rest [1 2 3]`).toEvaluateTo([2, 3], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.take returns first n elements', async () => {
|
test('list.take returns first n elements', async () => {
|
||||||
await expect(`list.take [1 2 3 4 5] 3`).toEvaluateTo([1, 2, 3])
|
await expect(`list.take [1 2 3 4 5] 3`).toEvaluateTo([1, 2, 3], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.drop skips first n elements', async () => {
|
test('list.drop skips first n elements', async () => {
|
||||||
await expect(`list.drop [1 2 3 4 5] 2`).toEvaluateTo([3, 4, 5])
|
await expect(`list.drop [1 2 3 4 5] 2`).toEvaluateTo([3, 4, 5], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.append adds to end', async () => {
|
test('list.append adds to end', async () => {
|
||||||
await expect(`list.append [1 2] 3`).toEvaluateTo([1, 2, 3])
|
await expect(`list.append [1 2] 3`).toEvaluateTo([1, 2, 3], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.prepend adds to start', async () => {
|
test('list.prepend adds to start', async () => {
|
||||||
await expect(`list.prepend [2 3] 1`).toEvaluateTo([1, 2, 3])
|
await expect(`list.prepend [2 3] 1`).toEvaluateTo([1, 2, 3], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.index-of finds element index', async () => {
|
test('list.index-of finds element index', async () => {
|
||||||
await expect(`list.index-of [1 2 3] 2`).toEvaluateTo(1)
|
await expect(`list.index-of [1 2 3] 2`).toEvaluateTo(1, globals)
|
||||||
await expect(`list.index-of [1 2 3] 5`).toEvaluateTo(-1)
|
await expect(`list.index-of [1 2 3] 5`).toEvaluateTo(-1, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.push adds to end and mutates array', async () => {
|
test('list.push adds to end and mutates array', async () => {
|
||||||
await expect(`arr = [1 2]; list.push arr 3; arr`).toEvaluateTo([1, 2, 3])
|
await expect(`arr = [1 2]; list.push arr 3; arr`).toEvaluateTo([1, 2, 3], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.push returns the size of the array', async () => {
|
test('list.push returns the size of the array', async () => {
|
||||||
await expect(`arr = [1 2]; arr | list.push 3`).toEvaluateTo(3)
|
await expect(`arr = [1 2]; arr | list.push 3`).toEvaluateTo(3, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.pop removes from end and mutates array', async () => {
|
test('list.pop removes from end and mutates array', async () => {
|
||||||
await expect(`arr = [1 2 3]; list.pop arr; arr`).toEvaluateTo([1, 2])
|
await expect(`arr = [1 2 3]; list.pop arr; arr`).toEvaluateTo([1, 2], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.pop returns removed element', async () => {
|
test('list.pop returns removed element', async () => {
|
||||||
await expect(`list.pop [1 2 3]`).toEvaluateTo(3)
|
await expect(`list.pop [1 2 3]`).toEvaluateTo(3, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.pop returns null for empty array', async () => {
|
test('list.pop returns null for empty array', async () => {
|
||||||
await expect(`list.pop []`).toEvaluateTo(null)
|
await expect(`list.pop []`).toEvaluateTo(null, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.shift removes from start and mutates array', async () => {
|
test('list.shift removes from start and mutates array', async () => {
|
||||||
await expect(`arr = [1 2 3]; list.shift arr; arr`).toEvaluateTo([2, 3])
|
await expect(`arr = [1 2 3]; list.shift arr; arr`).toEvaluateTo([2, 3], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.shift returns removed element', async () => {
|
test('list.shift returns removed element', async () => {
|
||||||
await expect(`list.shift [1 2 3]`).toEvaluateTo(1)
|
await expect(`list.shift [1 2 3]`).toEvaluateTo(1, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.shift returns null for empty array', async () => {
|
test('list.shift returns null for empty array', async () => {
|
||||||
await expect(`list.shift []`).toEvaluateTo(null)
|
await expect(`list.shift []`).toEvaluateTo(null, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.unshift adds to start and mutates array', async () => {
|
test('list.unshift adds to start and mutates array', async () => {
|
||||||
await expect(`arr = [2 3]; list.unshift arr 1; arr`).toEvaluateTo([1, 2, 3])
|
await expect(`arr = [2 3]; list.unshift arr 1; arr`).toEvaluateTo([1, 2, 3], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.unshift returns the length of the array', async () => {
|
test('list.unshift returns the length of the array', async () => {
|
||||||
await expect(`arr = [2 3]; arr | list.unshift 1`).toEvaluateTo(3)
|
await expect(`arr = [2 3]; arr | list.unshift 1`).toEvaluateTo(3, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.splice removes elements and mutates array', async () => {
|
test('list.splice removes elements and mutates array', async () => {
|
||||||
await expect(`arr = [1 2 3 4 5]; list.splice arr 1 2; arr`).toEvaluateTo([1, 4, 5])
|
await expect(`arr = [1 2 3 4 5]; list.splice arr 1 2; arr`).toEvaluateTo([1, 4, 5], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.splice returns removed elements', async () => {
|
test('list.splice returns removed elements', async () => {
|
||||||
await expect(`list.splice [1 2 3 4 5] 1 2`).toEvaluateTo([2, 3])
|
await expect(`list.splice [1 2 3 4 5] 1 2`).toEvaluateTo([2, 3], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.splice from start', async () => {
|
test('list.splice from start', async () => {
|
||||||
await expect(`list.splice [1 2 3 4 5] 0 2`).toEvaluateTo([1, 2])
|
await expect(`list.splice [1 2 3 4 5] 0 2`).toEvaluateTo([1, 2], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.splice to end', async () => {
|
test('list.splice to end', async () => {
|
||||||
await expect(`arr = [1 2 3 4 5]; list.splice arr 3 2; arr`).toEvaluateTo([1, 2, 3])
|
await expect(`arr = [1 2 3 4 5]; list.splice arr 3 2; arr`).toEvaluateTo([1, 2, 3], globals)
|
||||||
})
|
|
||||||
|
|
||||||
test('list.insert adds element at index and mutates array', async () => {
|
|
||||||
await expect(`arr = [1 2 4 5]; list.insert arr 2 3; arr`).toEvaluateTo([1, 2, 3, 4, 5])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.insert returns array length', async () => {
|
|
||||||
await expect(`list.insert [1 2 4] 2 3`).toEvaluateTo(4)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.insert at start', async () => {
|
|
||||||
await expect(`arr = [2 3]; list.insert arr 0 1; arr`).toEvaluateTo([1, 2, 3])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('list.insert at end', async () => {
|
|
||||||
await expect(`arr = [1 2]; list.insert arr 2 99; arr`).toEvaluateTo([1, 2, 99])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.sort with no callback sorts ascending', async () => {
|
test('list.sort with no callback sorts ascending', async () => {
|
||||||
await expect(`list.sort [3 1 4 1 5] null`).toEvaluateTo([1, 1, 3, 4, 5])
|
await expect(`list.sort [3 1 4 1 5] null`).toEvaluateTo([1, 1, 3, 4, 5], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.sort with callback sorts using comparator', async () => {
|
test('list.sort with callback sorts using comparator', async () => {
|
||||||
|
|
@ -375,7 +349,7 @@ describe('collections', () => {
|
||||||
b - a
|
b - a
|
||||||
end
|
end
|
||||||
list.sort [3 1 4 1 5] desc
|
list.sort [3 1 4 1 5] desc
|
||||||
`).toEvaluateTo([5, 4, 3, 1, 1])
|
`).toEvaluateTo([5, 4, 3, 1, 1], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.sort with callback for strings by length', async () => {
|
test('list.sort with callback for strings by length', async () => {
|
||||||
|
|
@ -384,52 +358,52 @@ describe('collections', () => {
|
||||||
(length a) - (length b)
|
(length a) - (length b)
|
||||||
end
|
end
|
||||||
list.sort ['cat' 'a' 'dog' 'elephant'] by-length
|
list.sort ['cat' 'a' 'dog' 'elephant'] by-length
|
||||||
`).toEvaluateTo(['a', 'cat', 'dog', 'elephant'])
|
`).toEvaluateTo(['a', 'cat', 'dog', 'elephant'], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.any? checks if any element matches', async () => {
|
test('list.any? checks if any element matches', async () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
gt-three = do x: x > 3 end
|
gt-three = do x: x > 3 end
|
||||||
list.any? [1 2 4 5] gt-three
|
list.any? [1 2 4 5] gt-three
|
||||||
`).toEvaluateTo(true)
|
`).toEvaluateTo(true, globals)
|
||||||
await expect(`
|
await expect(`
|
||||||
gt-ten = do x: x > 10 end
|
gt-ten = do x: x > 10 end
|
||||||
list.any? [1 2 3] gt-ten
|
list.any? [1 2 3] gt-ten
|
||||||
`).toEvaluateTo(false)
|
`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.all? checks if all elements match', async () => {
|
test('list.all? checks if all elements match', async () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
positive = do x: x > 0 end
|
positive = do x: x > 0 end
|
||||||
list.all? [1 2 3] positive
|
list.all? [1 2 3] positive
|
||||||
`).toEvaluateTo(true)
|
`).toEvaluateTo(true, globals)
|
||||||
await expect(`
|
await expect(`
|
||||||
positive = do x: x > 0 end
|
positive = do x: x > 0 end
|
||||||
list.all? [1 -2 3] positive
|
list.all? [1 -2 3] positive
|
||||||
`).toEvaluateTo(false)
|
`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.sum adds all numbers', async () => {
|
test('list.sum adds all numbers', async () => {
|
||||||
await expect(`list.sum [1 2 3 4]`).toEvaluateTo(10)
|
await expect(`list.sum [1 2 3 4]`).toEvaluateTo(10, globals)
|
||||||
await expect(`list.sum []`).toEvaluateTo(0)
|
await expect(`list.sum []`).toEvaluateTo(0, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.count counts matching elements', async () => {
|
test('list.count counts matching elements', async () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
gt-two = do x: x > 2 end
|
gt-two = do x: x > 2 end
|
||||||
list.count [1 2 3 4 5] gt-two
|
list.count [1 2 3 4 5] gt-two
|
||||||
`).toEvaluateTo(3)
|
`).toEvaluateTo(3, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.partition splits array by predicate', async () => {
|
test('list.partition splits array by predicate', async () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
gt-two = do x: x > 2 end
|
gt-two = do x: x > 2 end
|
||||||
list.partition [1 2 3 4 5] gt-two
|
list.partition [1 2 3 4 5] gt-two
|
||||||
`).toEvaluateTo([[3, 4, 5], [1, 2]])
|
`).toEvaluateTo([[3, 4, 5], [1, 2]], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.compact removes null values', async () => {
|
test('list.compact removes null values', async () => {
|
||||||
await expect(`list.compact [1 null 2 null 3]`).toEvaluateTo([1, 2, 3])
|
await expect(`list.compact [1 null 2 null 3]`).toEvaluateTo([1, 2, 3], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('list.group-by groups by key function', async () => {
|
test('list.group-by groups by key function', async () => {
|
||||||
|
|
@ -442,7 +416,7 @@ describe('collections', () => {
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
list.group-by ['a' 1 'b' 2] get-type
|
list.group-by ['a' 1 'b' 2] get-type
|
||||||
`).toEvaluateTo({ str: ['a', 'b'], num: [1, 2] })
|
`).toEvaluateTo({ str: ['a', 'b'], num: [1, 2] }, globals)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -451,14 +425,14 @@ describe('enumerables', () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
double = do x: x * 2 end
|
double = do x: x * 2 end
|
||||||
list.map [1 2 3] double
|
list.map [1 2 3] double
|
||||||
`).toEvaluateTo([2, 4, 6])
|
`).toEvaluateTo([2, 4, 6], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('map handles empty array', async () => {
|
test('map handles empty array', async () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
double = do x: x * 2 end
|
double = do x: x * 2 end
|
||||||
list.map [] double
|
list.map [] double
|
||||||
`).toEvaluateTo([])
|
`).toEvaluateTo([], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('each iterates over array', async () => {
|
test('each iterates over array', async () => {
|
||||||
|
|
@ -467,146 +441,165 @@ describe('enumerables', () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
double = do x: x * 2 end
|
double = do x: x * 2 end
|
||||||
each [1 2 3] double
|
each [1 2 3] double
|
||||||
`).toEvaluateTo([1, 2, 3])
|
`).toEvaluateTo([1, 2, 3], globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('each handles empty array', async () => {
|
test('each handles empty array', async () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
fn = do x: x end
|
fn = do x: x end
|
||||||
each [] fn
|
each [] fn
|
||||||
`).toEvaluateTo([])
|
`).toEvaluateTo([], globals)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('dict operations', () => {
|
describe('dict operations', () => {
|
||||||
test('dict.keys returns all keys', async () => {
|
test('dict.keys returns all keys', async () => {
|
||||||
await expect(`dict.keys [a=1 b=2 c=3] | list.sort`).toEvaluateTo(['a', 'b', 'c'].sort())
|
const result = await (async () => {
|
||||||
|
const { Compiler } = await import('#compiler/compiler')
|
||||||
|
const { run, fromValue } = await import('reefvm')
|
||||||
|
const { setGlobals } = await import('#parser/tokenizer')
|
||||||
|
setGlobals(Object.keys(globals))
|
||||||
|
const c = new Compiler('dict.keys [a=1 b=2 c=3]')
|
||||||
|
const r = await run(c.bytecode, globals)
|
||||||
|
return fromValue(r)
|
||||||
|
})()
|
||||||
|
// Check that all expected keys are present (order may vary)
|
||||||
|
expect(result.sort()).toEqual(['a', 'b', 'c'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dict.values returns all values', async () => {
|
test('dict.values returns all values', async () => {
|
||||||
await expect('dict.values [a=1 b=2] | list.sort').toEvaluateTo([1, 2].sort())
|
const result = await (async () => {
|
||||||
|
const { Compiler } = await import('#compiler/compiler')
|
||||||
|
const { run, fromValue } = await import('reefvm')
|
||||||
|
const { setGlobals } = await import('#parser/tokenizer')
|
||||||
|
setGlobals(Object.keys(globals))
|
||||||
|
const c = new Compiler('dict.values [a=1 b=2]')
|
||||||
|
const r = await run(c.bytecode, globals)
|
||||||
|
return fromValue(r)
|
||||||
|
})()
|
||||||
|
// Check that all expected values are present (order may vary)
|
||||||
|
expect(result.sort()).toEqual([1, 2])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dict.has? checks for key', async () => {
|
test('dict.has? checks for key', async () => {
|
||||||
await expect(`dict.has? [a=1 b=2] 'a'`).toEvaluateTo(true)
|
await expect(`dict.has? [a=1 b=2] 'a'`).toEvaluateTo(true, globals)
|
||||||
await expect(`dict.has? [a=1 b=2] 'c'`).toEvaluateTo(false)
|
await expect(`dict.has? [a=1 b=2] 'c'`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dict.get retrieves value with default', async () => {
|
test('dict.get retrieves value with default', async () => {
|
||||||
await expect(`dict.get [a=1] 'a' 0`).toEvaluateTo(1)
|
await expect(`dict.get [a=1] 'a' 0`).toEvaluateTo(1, globals)
|
||||||
await expect(`dict.get [a=1] 'b' 99`).toEvaluateTo(99)
|
await expect(`dict.get [a=1] 'b' 99`).toEvaluateTo(99, globals)
|
||||||
await expect(`dict.get [a=1] 'b'`).toEvaluateTo(null)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dict.set sets value', async () => {
|
test('dict.set sets value', async () => {
|
||||||
await expect(`map = [a=1]; dict.set map 'b' 99; map.b`).toEvaluateTo(99)
|
await expect(`map = [a=1]; dict.set map 'b' 99; map.b`).toEvaluateTo(99, globals)
|
||||||
await expect(`map = [a=1]; dict.set map 'a' 100; map.a`).toEvaluateTo(100)
|
await expect(`map = [a=1]; dict.set map 'a' 100; map.a`).toEvaluateTo(100, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dict.empty? checks if dict is empty', async () => {
|
test('dict.empty? checks if dict is empty', async () => {
|
||||||
await expect(`dict.empty? [=]`).toEvaluateTo(true)
|
await expect(`dict.empty? [=]`).toEvaluateTo(true, globals)
|
||||||
await expect(`dict.empty? [a=1]`).toEvaluateTo(false)
|
await expect(`dict.empty? [a=1]`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dict.merge combines dicts', async () => {
|
test('dict.merge combines dicts', async () => {
|
||||||
await expect(`dict.merge [a=1] [b=2]`).toEvaluateTo({ a: 1, b: 2 })
|
await expect(`dict.merge [a=1] [b=2]`).toEvaluateTo({ a: 1, b: 2 }, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dict.map transforms values', async () => {
|
test('dict.map transforms values', async () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
double = do v k: v * 2 end
|
double = do v k: v * 2 end
|
||||||
dict.map [a=1 b=2] double
|
dict.map [a=1 b=2] double
|
||||||
`).toEvaluateTo({ a: 2, b: 4 })
|
`).toEvaluateTo({ a: 2, b: 4 }, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dict.filter keeps matching entries', async () => {
|
test('dict.filter keeps matching entries', async () => {
|
||||||
await expect(`
|
await expect(`
|
||||||
gt-one = do v k: v > 1 end
|
gt-one = do v k: v > 1 end
|
||||||
dict.filter [a=1 b=2 c=3] gt-one
|
dict.filter [a=1 b=2 c=3] gt-one
|
||||||
`).toEvaluateTo({ b: 2, c: 3 })
|
`).toEvaluateTo({ b: 2, c: 3 }, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dict.from-entries creates dict from array', async () => {
|
test('dict.from-entries creates dict from array', async () => {
|
||||||
await expect(`dict.from-entries [['a' 1] ['b' 2]]`).toEvaluateTo({ a: 1, b: 2 })
|
await expect(`dict.from-entries [['a' 1] ['b' 2]]`).toEvaluateTo({ a: 1, b: 2 }, globals)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('math operations', () => {
|
describe('math operations', () => {
|
||||||
test('math.abs returns absolute value', async () => {
|
test('math.abs returns absolute value', async () => {
|
||||||
await expect(`math.abs -5`).toEvaluateTo(5)
|
await expect(`math.abs -5`).toEvaluateTo(5, globals)
|
||||||
await expect(`math.abs 5`).toEvaluateTo(5)
|
await expect(`math.abs 5`).toEvaluateTo(5, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.floor rounds down', async () => {
|
test('math.floor rounds down', async () => {
|
||||||
await expect(`math.floor 3.7`).toEvaluateTo(3)
|
await expect(`math.floor 3.7`).toEvaluateTo(3, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.ceil rounds up', async () => {
|
test('math.ceil rounds up', async () => {
|
||||||
await expect(`math.ceil 3.2`).toEvaluateTo(4)
|
await expect(`math.ceil 3.2`).toEvaluateTo(4, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.round rounds to nearest', async () => {
|
test('math.round rounds to nearest', async () => {
|
||||||
await expect(`math.round 3.4`).toEvaluateTo(3)
|
await expect(`math.round 3.4`).toEvaluateTo(3, globals)
|
||||||
await expect(`math.round 3.6`).toEvaluateTo(4)
|
await expect(`math.round 3.6`).toEvaluateTo(4, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.min returns minimum', async () => {
|
test('math.min returns minimum', async () => {
|
||||||
await expect(`math.min 5 2 8 1`).toEvaluateTo(1)
|
await expect(`math.min 5 2 8 1`).toEvaluateTo(1, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.max returns maximum', async () => {
|
test('math.max returns maximum', async () => {
|
||||||
await expect(`math.max 5 2 8 1`).toEvaluateTo(8)
|
await expect(`math.max 5 2 8 1`).toEvaluateTo(8, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.pow computes power', async () => {
|
test('math.pow computes power', async () => {
|
||||||
await expect(`math.pow 2 3`).toEvaluateTo(8)
|
await expect(`math.pow 2 3`).toEvaluateTo(8, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.sqrt computes square root', async () => {
|
test('math.sqrt computes square root', async () => {
|
||||||
await expect(`math.sqrt 16`).toEvaluateTo(4)
|
await expect(`math.sqrt 16`).toEvaluateTo(4, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.even? checks if even', async () => {
|
test('math.even? checks if even', async () => {
|
||||||
await expect(`math.even? 4`).toEvaluateTo(true)
|
await expect(`math.even? 4`).toEvaluateTo(true, globals)
|
||||||
await expect(`math.even? 5`).toEvaluateTo(false)
|
await expect(`math.even? 5`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.odd? checks if odd', async () => {
|
test('math.odd? checks if odd', async () => {
|
||||||
await expect(`math.odd? 5`).toEvaluateTo(true)
|
await expect(`math.odd? 5`).toEvaluateTo(true, globals)
|
||||||
await expect(`math.odd? 4`).toEvaluateTo(false)
|
await expect(`math.odd? 4`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.positive? checks if positive', async () => {
|
test('math.positive? checks if positive', async () => {
|
||||||
await expect(`math.positive? 5`).toEvaluateTo(true)
|
await expect(`math.positive? 5`).toEvaluateTo(true, globals)
|
||||||
await expect(`math.positive? -5`).toEvaluateTo(false)
|
await expect(`math.positive? -5`).toEvaluateTo(false, globals)
|
||||||
await expect(`math.positive? 0`).toEvaluateTo(false)
|
await expect(`math.positive? 0`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.negative? checks if negative', async () => {
|
test('math.negative? checks if negative', async () => {
|
||||||
await expect(`math.negative? -5`).toEvaluateTo(true)
|
await expect(`math.negative? -5`).toEvaluateTo(true, globals)
|
||||||
await expect(`math.negative? 5`).toEvaluateTo(false)
|
await expect(`math.negative? 5`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.zero? checks if zero', async () => {
|
test('math.zero? checks if zero', async () => {
|
||||||
await expect(`math.zero? 0`).toEvaluateTo(true)
|
await expect(`math.zero? 0`).toEvaluateTo(true, globals)
|
||||||
await expect(`math.zero? 5`).toEvaluateTo(false)
|
await expect(`math.zero? 5`).toEvaluateTo(false, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.clamp restricts value to range', async () => {
|
test('math.clamp restricts value to range', async () => {
|
||||||
await expect(`math.clamp 5 0 10`).toEvaluateTo(5)
|
await expect(`math.clamp 5 0 10`).toEvaluateTo(5, globals)
|
||||||
await expect(`math.clamp -5 0 10`).toEvaluateTo(0)
|
await expect(`math.clamp -5 0 10`).toEvaluateTo(0, globals)
|
||||||
await expect(`math.clamp 15 0 10`).toEvaluateTo(10)
|
await expect(`math.clamp 15 0 10`).toEvaluateTo(10, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.sign returns sign of number', async () => {
|
test('math.sign returns sign of number', async () => {
|
||||||
await expect(`math.sign 5`).toEvaluateTo(1)
|
await expect(`math.sign 5`).toEvaluateTo(1, globals)
|
||||||
await expect(`math.sign -5`).toEvaluateTo(-1)
|
await expect(`math.sign -5`).toEvaluateTo(-1, globals)
|
||||||
await expect(`math.sign 0`).toEvaluateTo(0)
|
await expect(`math.sign 0`).toEvaluateTo(0, globals)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('math.trunc truncates decimal', async () => {
|
test('math.trunc truncates decimal', async () => {
|
||||||
await expect(`math.trunc 3.7`).toEvaluateTo(3)
|
await expect(`math.trunc 3.7`).toEvaluateTo(3, globals)
|
||||||
await expect(`math.trunc -3.7`).toEvaluateTo(-3)
|
await expect(`math.trunc -3.7`).toEvaluateTo(-3, globals)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe } from 'bun:test'
|
import { describe } from 'bun:test'
|
||||||
import { expect, test } from 'bun:test'
|
import { expect, test } from 'bun:test'
|
||||||
import { Shrimp, runCode, compileCode, parseCode, bytecodeToString } from '..'
|
import { Shrimp } from '..'
|
||||||
|
|
||||||
describe('Shrimp', () => {
|
describe('Shrimp', () => {
|
||||||
test('allows running Shrimp code', async () => {
|
test('allows running Shrimp code', async () => {
|
||||||
|
|
@ -50,403 +50,4 @@ describe('Shrimp', () => {
|
||||||
await shrimp.run('abc = nothing')
|
await shrimp.run('abc = nothing')
|
||||||
expect(shrimp.get('abc')).toEqual('nothing')
|
expect(shrimp.get('abc')).toEqual('nothing')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('set()', () => {
|
|
||||||
test('allows setting variables', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
shrimp.set('foo', 42)
|
|
||||||
expect(shrimp.get('foo')).toEqual(42)
|
|
||||||
|
|
||||||
shrimp.set('bar', 'hello')
|
|
||||||
expect(shrimp.get('bar')).toEqual('hello')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('set variables are accessible in code', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
shrimp.set('x', 10)
|
|
||||||
shrimp.set('y', 20)
|
|
||||||
|
|
||||||
const result = await shrimp.run('x + y')
|
|
||||||
expect(result).toEqual(30)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('allows setting functions', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
shrimp.set('double', (n: number) => n * 2)
|
|
||||||
|
|
||||||
const result = await shrimp.run('double 21')
|
|
||||||
expect(result).toEqual(42)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('overwrites existing variables', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
await shrimp.run('x = 100')
|
|
||||||
expect(shrimp.get('x')).toEqual(100)
|
|
||||||
|
|
||||||
shrimp.set('x', 200)
|
|
||||||
expect(shrimp.get('x')).toEqual(200)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('has()', () => {
|
|
||||||
test('returns true for existing variables', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
await shrimp.run('x = 5')
|
|
||||||
expect(shrimp.has('x')).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns false for non-existing variables', () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
expect(shrimp.has('nonexistent')).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns true for globals', () => {
|
|
||||||
const shrimp = new Shrimp({ myGlobal: 42 })
|
|
||||||
|
|
||||||
expect(shrimp.has('myGlobal')).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns true for prelude functions', () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
expect(shrimp.has('echo')).toEqual(true)
|
|
||||||
expect(shrimp.has('type')).toEqual(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('call()', () => {
|
|
||||||
test('calls Shrimp functions with positional args', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
await shrimp.run(`add = do x y:
|
|
||||||
x + y
|
|
||||||
end`)
|
|
||||||
|
|
||||||
const result = await shrimp.call('add', 5, 10)
|
|
||||||
expect(result).toEqual(15)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('calls Shrimp functions with named args', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
await shrimp.run(`greet = do name:
|
|
||||||
str.join [ 'Hello ' name ] ''
|
|
||||||
end`)
|
|
||||||
|
|
||||||
const result = await shrimp.call('greet', { name: 'World' })
|
|
||||||
expect(result).toEqual('Hello World')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('calls native functions', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
shrimp.set('multiply', (a: number, b: number) => a * b)
|
|
||||||
|
|
||||||
const result = await shrimp.call('multiply', 6, 7)
|
|
||||||
expect(result).toEqual(42)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('calls prelude functions', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
const result = await shrimp.call('type', 42)
|
|
||||||
expect(result).toEqual('number')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('calls async functions', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
shrimp.set('fetchData', async () => {
|
|
||||||
return await Promise.resolve('async data')
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await shrimp.call('fetchData')
|
|
||||||
expect(result).toEqual('async data')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('compile()', () => {
|
|
||||||
test('compiles code to bytecode', () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
const bytecode = shrimp.compile('x = 5')
|
|
||||||
|
|
||||||
expect(bytecode).toHaveProperty('instructions')
|
|
||||||
expect(bytecode).toHaveProperty('constants')
|
|
||||||
expect(bytecode).toHaveProperty('labels')
|
|
||||||
expect(bytecode.instructions.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('respects globals when compiling', () => {
|
|
||||||
const shrimp = new Shrimp({ customGlobal: 42 })
|
|
||||||
|
|
||||||
const bytecode = shrimp.compile('x = customGlobal')
|
|
||||||
expect(bytecode.instructions.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('compiled bytecode can be run', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
const bytecode = shrimp.compile('2 * 21')
|
|
||||||
const result = await shrimp.run(bytecode)
|
|
||||||
|
|
||||||
expect(result).toEqual(42)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('parse()', () => {
|
|
||||||
test('parses code to syntax tree', () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
const tree = shrimp.parse('x = 5')
|
|
||||||
|
|
||||||
expect(tree).toHaveProperty('length')
|
|
||||||
expect(tree).toHaveProperty('cursor')
|
|
||||||
expect(tree.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('respects globals when parsing', () => {
|
|
||||||
const shrimp = new Shrimp({ myVar: 42 })
|
|
||||||
|
|
||||||
const tree = shrimp.parse('x = myVar + 10')
|
|
||||||
|
|
||||||
// Should parse without errors
|
|
||||||
expect(tree).toHaveProperty('length')
|
|
||||||
expect(tree.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses function definitions', () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
const tree = shrimp.parse(`add = do x y:
|
|
||||||
x + y
|
|
||||||
end`)
|
|
||||||
|
|
||||||
expect(tree.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('get()', () => {
|
|
||||||
test('returns null for undefined variables', () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
expect(shrimp.get('undefined')).toEqual(null)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns values from code execution', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
await shrimp.run('x = 42')
|
|
||||||
expect(shrimp.get('x')).toEqual(42)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns arrays', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
await shrimp.run('arr = [1 2 3]')
|
|
||||||
expect(shrimp.get('arr')).toEqual([1, 2, 3])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns dicts', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
await shrimp.run('dict = [a=1 b=2]')
|
|
||||||
expect(shrimp.get('dict')).toEqual({ a: 1, b: 2 })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('running bytecode directly', () => {
|
|
||||||
test('can run pre-compiled bytecode', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
const bytecode = shrimp.compile('x = 100')
|
|
||||||
const result = await shrimp.run(bytecode)
|
|
||||||
|
|
||||||
expect(result).toEqual(100)
|
|
||||||
expect(shrimp.get('x')).toEqual(100)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('maintains state across bytecode runs', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
const bytecode1 = shrimp.compile('x = 10')
|
|
||||||
const bytecode2 = shrimp.compile('x + 5')
|
|
||||||
|
|
||||||
await shrimp.run(bytecode1)
|
|
||||||
const result = await shrimp.run(bytecode2)
|
|
||||||
|
|
||||||
expect(result).toEqual(15)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Functional API', () => {
|
|
||||||
describe('runCode()', () => {
|
|
||||||
test('runs code and returns result', async () => {
|
|
||||||
const result = await runCode('1 + 1')
|
|
||||||
expect(result).toEqual(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('works with globals', async () => {
|
|
||||||
const result = await runCode('greet', { greet: () => 'hello' })
|
|
||||||
expect(result).toEqual('hello')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('has access to prelude', async () => {
|
|
||||||
const result = await runCode('type 42')
|
|
||||||
expect(result).toEqual('number')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns null for empty code', async () => {
|
|
||||||
const result = await runCode('')
|
|
||||||
expect(result).toEqual(null)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('compileCode()', () => {
|
|
||||||
test('compiles code to bytecode', () => {
|
|
||||||
const bytecode = compileCode('x = 5')
|
|
||||||
|
|
||||||
expect(bytecode).toHaveProperty('instructions')
|
|
||||||
expect(bytecode).toHaveProperty('constants')
|
|
||||||
expect(bytecode.instructions.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('respects globals', () => {
|
|
||||||
const bytecode = compileCode('x = myGlobal', { myGlobal: 42 })
|
|
||||||
|
|
||||||
expect(bytecode.instructions.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('compiled bytecode is usable', async () => {
|
|
||||||
const bytecode = compileCode('21 * 2')
|
|
||||||
const result = await runCode('21 * 2')
|
|
||||||
|
|
||||||
expect(result).toEqual(42)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('parseCode()', () => {
|
|
||||||
test('parses code to syntax tree', () => {
|
|
||||||
const tree = parseCode('x = 5')
|
|
||||||
|
|
||||||
expect(tree).toHaveProperty('length')
|
|
||||||
expect(tree.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('respects globals', () => {
|
|
||||||
const tree = parseCode('x = myGlobal', { myGlobal: 42 })
|
|
||||||
|
|
||||||
expect(tree.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('handles complex expressions', () => {
|
|
||||||
const tree = parseCode(`add = do x y:
|
|
||||||
x + y
|
|
||||||
end
|
|
||||||
result = add 5 10`)
|
|
||||||
|
|
||||||
expect(tree.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('bytecodeToString()', () => {
|
|
||||||
test('converts bytecode to human-readable format', () => {
|
|
||||||
const bytecode = compileCode('x = 42')
|
|
||||||
const str = bytecodeToString(bytecode)
|
|
||||||
|
|
||||||
expect(typeof str).toEqual('string')
|
|
||||||
expect(str.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows instructions', () => {
|
|
||||||
const bytecode = compileCode('1 + 1')
|
|
||||||
const str = bytecodeToString(bytecode)
|
|
||||||
|
|
||||||
// Should contain some opcodes
|
|
||||||
expect(str).toContain('PUSH')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Integration tests', () => {
|
|
||||||
test('complex REPL-like workflow', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
// Define a function
|
|
||||||
await shrimp.run(`double = do x:
|
|
||||||
x * 2
|
|
||||||
end`)
|
|
||||||
expect(shrimp.has('double')).toEqual(true)
|
|
||||||
|
|
||||||
// Use the function
|
|
||||||
const result1 = await shrimp.run('double 21')
|
|
||||||
expect(result1).toEqual(42)
|
|
||||||
|
|
||||||
// Call it from TypeScript
|
|
||||||
const result2 = await shrimp.call('double', 50)
|
|
||||||
expect(result2).toEqual(100)
|
|
||||||
|
|
||||||
// Define another function using the first
|
|
||||||
await shrimp.run(`quadruple = do x:
|
|
||||||
double (double x)
|
|
||||||
end`)
|
|
||||||
|
|
||||||
const result3 = await shrimp.run('quadruple 5')
|
|
||||||
expect(result3).toEqual(20)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('mixing native and Shrimp functions', async () => {
|
|
||||||
const shrimp = new Shrimp({
|
|
||||||
log: (msg: string) => `Logged: ${msg}`,
|
|
||||||
multiply: (a: number, b: number) => a * b,
|
|
||||||
})
|
|
||||||
|
|
||||||
await shrimp.run(`greet = do name:
|
|
||||||
log name
|
|
||||||
end`)
|
|
||||||
|
|
||||||
const result1 = await shrimp.run('greet Alice')
|
|
||||||
expect(result1).toEqual('Logged: Alice')
|
|
||||||
|
|
||||||
await shrimp.run(`calc = do x:
|
|
||||||
multiply x 3
|
|
||||||
end`)
|
|
||||||
|
|
||||||
const result2 = await shrimp.run('calc 7')
|
|
||||||
expect(result2).toEqual(21)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('working with arrays and dicts', async () => {
|
|
||||||
const shrimp = new Shrimp()
|
|
||||||
|
|
||||||
await shrimp.run('nums = [1 2 3 4 5]')
|
|
||||||
expect(shrimp.get('nums')).toEqual([1, 2, 3, 4, 5])
|
|
||||||
|
|
||||||
await shrimp.run("config = [host='localhost' port=3000]")
|
|
||||||
expect(shrimp.get('config')).toEqual({ host: 'localhost', port: 3000 })
|
|
||||||
|
|
||||||
const result = await shrimp.run('length nums')
|
|
||||||
expect(result).toEqual(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('compile once, run multiple times', async () => {
|
|
||||||
const bytecode = compileCode('x * 2')
|
|
||||||
|
|
||||||
const shrimp1 = new Shrimp()
|
|
||||||
shrimp1.set('x', 10)
|
|
||||||
const result1 = await shrimp1.run(bytecode)
|
|
||||||
expect(result1).toEqual(20)
|
|
||||||
|
|
||||||
const shrimp2 = new Shrimp()
|
|
||||||
shrimp2.set('x', 100)
|
|
||||||
const result2 = await shrimp2.run(bytecode)
|
|
||||||
expect(result2).toEqual(200)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user