Compare commits

...

34 Commits

Author SHA1 Message Date
Chris Wanstrath
4f092fca3f prelude tests 2025-10-28 12:51:46 -07:00
b50dafe79d need you 2025-10-28 12:51:46 -07:00
af7f389a04 use module 2025-10-28 12:51:46 -07:00
67a98837ed native -> global 2025-10-28 12:51:46 -07:00
b42675e1d9 narrow type 2025-10-28 12:51:46 -07:00
354df33894 no valueFunctions 2025-10-28 12:51:46 -07:00
66d21ce72b update-reef command 2025-10-28 12:51:46 -07:00
d7bcc590fe better echo 2025-10-28 12:51:46 -07:00
787d2f2611 start on a prelude of builtin functions 2025-10-28 12:51:46 -07:00
bbc9316074 Merge pull request 'Functions returned by parens expr can be called' (#10) from word-to-your-mother into main
Reviewed-on: #10
2025-10-27 23:08:54 +00:00
4a2e1f094a Merge pull request 'Remove ReefVM submodule and instead depend on the git repo as a package.' (#11) from reef-git-package into main
Reviewed-on: #11
2025-10-27 23:08:39 +00:00
Chris Wanstrath
825487f2e0 Remove ReefVM submodule and instead depend on the git repo as a package. 2025-10-27 15:24:48 -07:00
Chris Wanstrath
0788f830bc update git submodule URL 2025-10-27 15:20:31 -07:00
c032192d61 Functions returned by parens expr can be called 2025-10-27 15:17:38 -07:00
c6c2646366 Do do 2025-10-27 14:50:54 -07:00
71fdafa72d Merge pull request 'DotGet function calls' (#9) from dotget-function-calls into main
Reviewed-on: #9
2025-10-27 19:46:06 +00:00
318142dfbb Update shrimp.ts 2025-10-27 12:45:59 -07:00
ffdd666685 Merge remote-tracking branch 'origin/main' into dotget-function-calls 2025-10-27 12:45:53 -07:00
0fc1f9f895 Merge pull request 'allow more unicode in variable names' (#8) from more-unicode-variable-names into main
Reviewed-on: #8
2025-10-27 19:43:55 +00:00
cdcaf5c9d3 Merge pull request 'failing test for multiline function' (#6) from multiline-fn into main
Reviewed-on: #6
2025-10-27 19:37:40 +00:00
6c8c07e869 Update shrimp.ts 2025-10-27 12:36:29 -07:00
2fcd840493 Merge remote-tracking branch 'origin/main' into multiline-fn 2025-10-27 12:36:12 -07:00
28fab1235c Works with blank lines 2025-10-27 12:07:13 -07:00
cbd3fe6315 Merge pull request 'failing single line if test' (#5) from single-line-if into main
Reviewed-on: #5
2025-10-27 18:31:48 +00:00
6e432dd7a1 Made it work 2025-10-27 11:30:49 -07:00
050acbfaeb Merge remote-tracking branch 'origin/main' into single-line-if 2025-10-27 10:54:33 -07:00
34d1b8b998 Merge pull request 'Add # comments' (#4) from comments into main
Reviewed-on: #4
2025-10-27 17:50:29 +00:00
219142140c compile dot number too 2025-10-26 22:32:07 -07:00
972fd25fda update params scope detection, add array.1 (dotget array indices) 2025-10-26 22:23:46 -07:00
abd7d2e43b DotGet function calls 2025-10-26 16:28:56 -07:00
7cf7ac3703 allow more unicode in variable names 2025-10-26 13:03:17 -07:00
299ad2c9a9 failing test for multiline function 2025-10-25 20:15:55 -07:00
e4100c7d89 failing single line if test 2025-10-25 19:51:57 -07:00
dba8430d9a Add # comments 2025-10-25 19:18:27 -07:00
25 changed files with 1039 additions and 290 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "packages/ReefVM"]
path = packages/ReefVM
url = git@54.219.130.253:defunkt/ReefVM.git

View File

@ -1,24 +1,12 @@
#!/usr/bin/env bun
import { Compiler } from '../src/compiler/compiler'
import { VM, type Value, Scope, bytecodeToString } from 'reefvm'
import { colors, formatValue, globalFunctions } from '../src/prelude'
import { VM, Scope, bytecodeToString } from 'reefvm'
import * as readline from 'readline'
import { readFileSync, writeFileSync } from 'fs'
import { basename } from 'path'
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
cyan: '\x1b[36m',
yellow: '\x1b[33m',
green: '\x1b[32m',
red: '\x1b[31m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
pink: '\x1b[38;2;255;105;180m'
}
async function repl() {
const commands = ['/clear', '/reset', '/vars', '/funcs', '/history', '/bytecode', '/exit', '/save', '/quit']
@ -60,7 +48,7 @@ async function repl() {
return
}
vm ||= new VM({ instructions: [], constants: [] }, nativeFunctions)
vm ||= new VM({ instructions: [], constants: [] }, globalFunctions)
if (['/exit', 'exit', '/quit', 'quit'].includes(trimmed)) {
console.log(`\n${colors.yellow}Goodbye!${colors.reset}`)
@ -186,40 +174,6 @@ async function repl() {
})
}
function formatValue(value: Value, inner = false): string {
switch (value.type) {
case 'string':
return `${colors.green}'${value.value}'${colors.reset}`
case 'number':
return `${colors.cyan}${value.value}${colors.reset}`
case 'boolean':
return `${colors.yellow}${value.value}${colors.reset}`
case 'null':
return `${colors.dim}null${colors.reset}`
case 'array': {
const items = value.value.map(x => formatValue(x, true)).join(' ')
return `${inner ? '(' : ''}${colors.blue}list${colors.reset} ${items}${inner ? ')' : ''}`
}
case 'dict': {
const entries = Array.from(value.value.entries())
.map(([k, v]) => `${k}=${formatValue(v, true)}`)
.join(' ')
return `${inner ? '(' : ''}${colors.magenta}dict${colors.reset} ${entries}${inner ? ')' : ''}`
}
case 'function': {
const params = value.params.join(', ')
return `${colors.dim}<fn(${params})>${colors.reset}`
}
case 'native':
return `${colors.dim}<native-fn>${colors.reset}`
case 'regex':
return `${colors.magenta}${value.value}${colors.reset}`
default:
return String(value)
}
}
function formatVariables(scope: Scope, onlyFunctions = false): string {
const vars: string[] = []
@ -257,7 +211,7 @@ async function loadFile(filePath: string): Promise<{ vm: VM; codeHistory: string
console.log(`${colors.dim}Loading ${basename(filePath)}...${colors.reset}`)
const vm = new VM({ instructions: [], constants: [] }, nativeFunctions)
const vm = new VM({ instructions: [], constants: [] }, globalFunctions)
await vm.run()
const codeHistory: string[] = []
@ -313,43 +267,4 @@ function showWelcome() {
console.log()
}
const nativeFunctions = {
echo: (...args: any[]) => {
console.log(...args)
},
len: (value: any) => {
if (typeof value === 'string') return value.length
if (Array.isArray(value)) return value.length
if (value && typeof value === 'object') return Object.keys(value).length
return 0
},
type: (value: any) => {
if (value === null) return 'null'
if (Array.isArray(value)) return 'array'
return typeof value
},
range: (start: number, end: number | null) => {
if (end === null) {
end = start
start = 0
}
const result: number[] = []
for (let i = start; i <= end; i++) {
result.push(i)
}
return result
},
join: (arr: any[], sep: string = ',') => {
return arr.join(sep)
},
split: (str: string, sep: string = ',') => {
return str.split(sep)
},
upper: (str: string) => str.toUpperCase(),
lower: (str: string) => str.toLowerCase(),
trim: (str: string) => str.trim(),
list: (...args: any[]) => args,
dict: (atNamed = {}) => atNamed
}
await repl()

View File

@ -1,57 +1,18 @@
#!/usr/bin/env bun
import { Compiler } from '../src/compiler/compiler'
import { VM, toValue, fromValue, bytecodeToString } from 'reefvm'
import { colors, globalFunctions } from '../src/prelude'
import { VM, fromValue, bytecodeToString } from 'reefvm'
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
import { randomUUID } from "crypto"
import { spawn } from 'child_process'
import { join } from 'path'
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
yellow: '\x1b[33m',
cyan: '\x1b[36m',
magenta: '\x1b[35m',
pink: '\x1b[38;2;255;105;180m'
}
const nativeFunctions = {
echo: (...args: any[]) => console.log(...args),
len: (value: any) => {
if (typeof value === 'string') return value.length
if (Array.isArray(value)) return value.length
if (value && typeof value === 'object') return Object.keys(value).length
return 0
},
type: (value: any) => toValue(value).type,
range: (start: number, end: number | null) => {
if (end === null) {
end = start
start = 0
}
const result: number[] = []
for (let i = start; i <= end; i++) {
result.push(i)
}
return result
},
join: (arr: any[], sep: string = ',') => arr.join(sep),
split: (str: string, sep: string = ',') => str.split(sep),
upper: (str: string) => str.toUpperCase(),
lower: (str: string) => str.toLowerCase(),
trim: (str: string) => str.trim(),
list: (...args: any[]) => args,
dict: (atNamed = {}) => atNamed
}
async function runFile(filePath: string) {
try {
const code = readFileSync(filePath, 'utf-8')
const compiler = new Compiler(code)
const vm = new VM(compiler.bytecode, nativeFunctions)
const vm = new VM(compiler.bytecode, globalFunctions)
await vm.run()
return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]) : null
} catch (error: any) {

View File

@ -9,7 +9,7 @@
"bun-plugin-tailwind": "^0.0.15",
"codemirror": "^6.0.2",
"hono": "^4.9.8",
"reefvm": "workspace:*",
"reefvm": "git+https://git.nose.space/defunkt/reefvm",
"tailwindcss": "^4.1.11",
},
"devDependencies": {
@ -18,15 +18,6 @@
"@types/bun": "latest",
},
},
"packages/ReefVM": {
"name": "reefvm",
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.19.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg=="],
@ -71,7 +62,7 @@
"hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="],
"reefvm": ["reefvm@workspace:packages/ReefVM"],
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#97b6722a113417398a1c47d583bfe07a906f87a0", { "peerDependencies": { "typescript": "^5" } }, "97b6722a113417398a1c47d583bfe07a906f87a0"],
"style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="],
@ -82,9 +73,5 @@
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"reefvm/@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="],
"reefvm/@types/bun/bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
}
}

View File

@ -9,10 +9,11 @@
"scripts": {
"dev": "bun generate-parser && bun --hot src/server/server.tsx",
"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": "cd packages/ReefVM && git pull origin main"
},
"dependencies": {
"reefvm": "workspace:*",
"reefvm": "git+https://git.nose.space/defunkt/reefvm",
"@codemirror/view": "^6.38.3",
"@lezer/generator": "^1.8.0",
"bun-plugin-tailwind": "^0.0.15",

@ -1 +0,0 @@
Subproject commit 17d846b99910a46fc1c7ab98aa41ca8afbc14097

View File

@ -303,7 +303,8 @@ export class Compiler {
return instructions
}
case terms.ThenBlock: {
case terms.ThenBlock:
case terms.SingleLineThenBlock: {
const instructions = getAllChildren(node)
.map((child) => this.#compileNode(child, input))
.flat()
@ -468,7 +469,11 @@ export class Compiler {
}
default:
throw new CompilerError(`Unsupported syntax node: ${node.type.name}`, node.from, node.to)
throw new CompilerError(
`Compiler doesn't know how to handle a "${node.type.name}" node.`,
node.from,
node.to
)
}
}
}

View File

@ -85,6 +85,21 @@ describe('compiler', () => {
expect(`bloop = do: 'bloop' end; bloop`).toEvaluateTo('bloop')
})
test('function call with if statement and multiple expressions', () => {
expect(`
abc = do:
if false:
echo nope
end
true
end
abc
`)
.toEvaluateTo(true)
})
test('simple conditionals', () => {
expect(`(3 < 6)`).toEvaluateTo(true)
expect(`(10 > 20)`).toEvaluateTo(false)
@ -139,6 +154,10 @@ describe('compiler', () => {
scattered
end`).toEvaluateTo('dwarf')
})
test('single line if', () => {
expect(`if 3 < 9: shire end`).toEvaluateTo('shire')
})
})
describe('errors', () => {

View File

@ -70,9 +70,9 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => {
}
const paramNames = getAllChildren(paramsNode).map((param) => {
if (param.type.id !== terms.AssignableIdentifier) {
if (param.type.id !== terms.Identifier) {
throw new CompilerError(
`FunctionDef params must be AssignableIdentifiers, got ${param.type.name}`,
`FunctionDef params must be Identifier, got ${param.type.name}`,
param.from,
param.to
)
@ -219,9 +219,9 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
)
}
if (property.type.id !== terms.Identifier) {
if (property.type.id !== terms.Identifier && property.type.id !== terms.Number) {
throw new CompilerError(
`DotGet property must be an Identifier, got ${property.type.name}`,
`DotGet property must be an Identifier or Number, got ${property.type.name}`,
property.from,
property.to
)

View File

@ -57,26 +57,30 @@ const readIdentifierText = (input: InputStream, start: number, end: number): str
return text
}
let inParams = false
export const trackScope = new ContextTracker<TrackerContext>({
start: new TrackerContext(new Scope(null, new Set())),
shift(context, term, stack, input) {
if (term !== terms.AssignableIdentifier) return context
if (term == terms.Do) inParams = true
if (term === terms.AssignableIdentifier) {
const text = readIdentifierText(input, input.pos, stack.pos)
return new TrackerContext(Scope.add(context.scope, text), context.pendingIds)
}
if (inParams && term === terms.Identifier) {
const text = readIdentifierText(input, input.pos, stack.pos)
return new TrackerContext(context.scope, [...context.pendingIds, text])
}
return context
},
reduce(context, term) {
// Add assignment variable to scope
if (term === terms.Assign) {
const varName = context.pendingIds.at(-1)
if (!varName) return context
return new TrackerContext(Scope.add(context.scope, varName), context.pendingIds.slice(0, -1))
}
// Push new scope and add all parameters
if (term === terms.Params) {
inParams = false
let newScope = context.scope.push()
if (context.pendingIds.length > 0) {
newScope = Scope.add(newScope, ...context.pendingIds)

View File

@ -2,7 +2,7 @@
@context trackScope from "./scopeTracker"
@skip { space }
@skip { space | comment }
@top Program { item* }
@ -18,6 +18,7 @@
newlineOrSemicolon { "\n" | ";" }
eof { @eof }
space { " " | "\t" }
comment { "#" ![\n]* }
leftParen { "(" }
rightParen { ")" }
colon[closedBy="end", @name="colon"] { ":" }
@ -28,6 +29,7 @@
}
@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot }
@external specialize {Identifier} specializeKeyword from "./tokenizer" { Do }
@precedence {
pipe @left,
@ -46,7 +48,6 @@ item {
consumeToTerminator {
PipeExpr |
ambiguousFunctionCall |
DotGet |
IfExpr |
FunctionDef |
Assign |
@ -63,7 +64,7 @@ pipeOperand {
}
FunctionCallOrIdentifier {
Identifier
DotGet | Identifier
}
ambiguousFunctionCall {
@ -71,7 +72,7 @@ ambiguousFunctionCall {
}
FunctionCall {
Identifier arg+
(DotGet | Identifier | ParenExpr) arg+
}
arg {
@ -92,11 +93,11 @@ FunctionDef {
}
singleLineFunctionDef {
@specialize[@name=keyword]<Identifier, "do"> Params colon consumeToTerminator @specialize[@name=keyword]<Identifier, "end">
Do Params colon consumeToTerminator @specialize[@name=keyword]<Identifier, "end">
}
multilineFunctionDef {
@specialize[@name=keyword]<Identifier, "do"> Params colon newlineOrSemicolon block @specialize[@name=keyword]<Identifier, "end">
Do Params colon newlineOrSemicolon block @specialize[@name=keyword]<Identifier, "end">
}
IfExpr {
@ -104,7 +105,7 @@ IfExpr {
}
singleLineIf {
@specialize[@name=keyword]<Identifier, "if"> (ConditionalOp | expression) colon ThenBlock { consumeToTerminator }
@specialize[@name=keyword]<Identifier, "if"> (ConditionalOp | expression) colon SingleLineThenBlock @specialize[@name=keyword]<Identifier, "end">
}
multilineIf {
@ -123,6 +124,10 @@ ThenBlock {
block
}
SingleLineThenBlock {
consumeToTerminator
}
ConditionalOp {
expression Eq expression |
expression Neq expression |
@ -135,7 +140,7 @@ ConditionalOp {
}
Params {
AssignableIdentifier*
Identifier*
}
Assign {
@ -164,7 +169,7 @@ expression {
@skip {} {
DotGet {
IdentifierBeforeDot dot Identifier
IdentifierBeforeDot dot (Number | Identifier)
}
String { "'" stringContent* "'" }
@ -199,5 +204,5 @@ expressionWithoutIdentifier {
}
block {
(consumeToTerminator newlineOrSemicolon)*
(consumeToTerminator? newlineOrSemicolon)*
}

View File

@ -16,31 +16,33 @@ export const
AssignableIdentifier = 14,
Word = 15,
IdentifierBeforeDot = 16,
Program = 17,
PipeExpr = 18,
FunctionCall = 19,
PositionalArg = 20,
ParenExpr = 21,
FunctionCallOrIdentifier = 22,
BinOp = 23,
ConditionalOp = 24,
FunctionDef = 25,
Do = 17,
Program = 18,
PipeExpr = 19,
FunctionCall = 20,
DotGet = 21,
Number = 22,
ParenExpr = 23,
FunctionCallOrIdentifier = 24,
BinOp = 25,
String = 26,
StringFragment = 27,
Interpolation = 28,
EscapeSeq = 29,
Boolean = 30,
Regex = 31,
Null = 32,
ConditionalOp = 33,
FunctionDef = 34,
Params = 35,
colon = 36,
keyword = 50,
Params = 27,
colon = 28,
String = 30,
StringFragment = 31,
Interpolation = 32,
EscapeSeq = 33,
Number = 34,
Boolean = 35,
Regex = 36,
Null = 37,
DotGet = 38,
PositionalArg = 38,
Underscore = 39,
NamedArg = 40,
NamedArgPrefix = 41,
IfExpr = 43,
SingleLineThenBlock = 45,
ThenBlock = 46,
ElseIfExpr = 47,
ElseExpr = 49,

View File

@ -1,27 +1,27 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser, LocalTokenGroup} from "@lezer/lr"
import {operatorTokenizer} from "./operatorTokenizer"
import {tokenizer} from "./tokenizer"
import {tokenizer, specializeKeyword} from "./tokenizer"
import {trackScope} from "./scopeTracker"
import {highlighting} from "./highlight"
const spec_Identifier = {__proto__:null,do:52, end:58, null:74, if:88, elseif:96, else:100}
const spec_Identifier = {__proto__:null,null:64, end:74, if:88, elseif:96, else:100}
export const parser = LRParser.deserialize({
version: 14,
states: ".jQVQbOOO#XQcO'#CrO$RQRO'#CsO$aQcO'#DmO$xQbO'#DsOOQ`'#Cu'#CuO%QQbO'#CqO%rOSO'#CzOOQa'#Dq'#DqO&QOpO'#DSO&VQcO'#DpOOQ`'#Dn'#DnO&nQbO'#DmO&|QbO'#EQOOQ`'#DX'#DXO'kQRO'#DaOOQ`'#Dm'#DmO'pQQO'#DlOOQ`'#Dl'#DlOOQ`'#Db'#DbQVQbOOOOQa'#Dp'#DpOOQ`'#Cp'#CpO'xQbO'#DUOOQ`'#Do'#DoOOQ`'#Dc'#DcO(SQbO,59ZO&|QbO,59_O&|QbO,59_OOQ`'#Dd'#DdO(pQbO'#CwO(xQQO,5:_O)iQRO'#CsO)yQRO,59]O*[QRO,59]O*VQQO,59]O+VQQO,59]O+_QbO'#C|O+gQWO'#C}OOOO'#Dy'#DyOOOO'#Df'#DfO+{OSO,59fOOQa,59f,59fO,ZO`O,59nO,`QbO'#DgO,eQbO,59YO,vQRO,5:lO,}QQO,5:lO-SQbO,59{OOQ`,5:W,5:WOOQ`-E7`-E7`OOQ`,59p,59pOOQ`-E7a-E7aOOQa1G.y1G.yO-^QcO1G.yOOQ`-E7b-E7bO-xQbO1G/yO&|QbO,59`O&|QbO,59`OOQa1G.w1G.wOOOO,59h,59hOOOO,59i,59iOOOO-E7d-E7dOOQa1G/Q1G/QOOQa1G/Y1G/YO!QQbO'#CrOOQ`,5:R,5:ROOQ`-E7e-E7eO.VQbO1G0WOOQ`1G/g1G/gO.dQbO7+%eO.iQbO7+%fOOQO1G.z1G.zO.vQRO1G.zOOQ`'#DZ'#DZOOQ`7+%r7+%rO/QQbO7+%sOOQ`<<IP<<IPO/eQQO'#DeO/jQbO'#DvO/}QbO<<IQOOQ`'#D['#D[O0SQbO<<I_OOQ`,5:P,5:POOQ`-E7c-E7cOOQ`AN>lAN>lO&|QbO'#D]OOQ`'#Dh'#DhO0_QbOAN>yO0jQQO'#D_OOQ`AN>yAN>yO0oQbOAN>yO0tQRO,59wO0{QQO,59wOOQ`-E7f-E7fOOQ`G24eG24eO1QQbOG24eO1VQQO,59yO1[QQO1G/cOOQ`LD*PLD*PO.iQbO1G/eO/QQbO7+$}OOQ`7+%P7+%POOQ`<<Hi<<Hi",
stateData: "1d~O!_OS~O]PO^_O_WO`XOjSOrWOsWOtWOuWO|]O!fUO!ibO!lVO~O]eO_WO`XOjSOrWOsWOtWOuWOwfOygO!fUO!lVOzfX!ifX!vfX!kfXmfX~OP!dXQ!dXR!dXS!dXT!dXU!dXV!dXW!dXX!dXY!dXZ!dX[!dX~P!QOPkOQkORlOSlO~OPkOQkORlOSlO!i!aX!v!aXm!aX~O^mOlkP~O]PO_WO`XOjSOrWOsWOtWOuWO!fUO!lVO~OowO!lzO!nuO!ovO~O!s{O~OP!dXQ!dXR!dXS!dX!i!aX!v!aXm!aX~Oz|O!i!aX!v!aXm!aX~O]eO_WO`XOrWOsWOtWOuWO!fUO!lVO~OV!QO~O!i!RO!v!RO~OjSOw!TO~P&|OjSOwfOygOzca!ica!vca!kcamca~P&|O^mOlkX~Ol!YO~OT![OU![OV!ZOW!ZOX!ZOY!ZOZ!ZO[!ZO~OPkOQkORlOSlO~P(}OPkOQkORlOSlO!k!]O~O!k!]OP!dXQ!dXR!dXS!dXT!dXU!dXV!dXW!dXX!dXY!dXZ!dX[!dX~Oz|O!k!]O~O]!^O!fUO~O!l!_O!n!_O!o!_O!p!_O!q!_O!r!_O~OowO!l!aO!nuO!ovO~O]!bO~O]!cO~Oz|O!iba!vba!kbamba~Ol!fO~P(}Ol!fO~O^_O|]O~P%QOPkOQkORgiSgi!igi!vgi!kgimgi~O^_O|]O!i!iO~P%QO^_O|]O!i!nO~P%QOm!oO~O^_O|]Om!jP~P%QO!khilhi~P(}O^_O|]Om!jP!Q!jP!S!jP~P%QO!i!uO~O^_O|]Om!jX!Q!jX!S!jX~P%QOm!wO~Om!|O!Q!xO!S!{O~Om#RO!Q!xO!S!{O~Ol#TO~Om#RO~Ol#UO~P(}Ol#UO~Om#VO~O!i#WO~O!i#XO~Ort~",
goto: "+q!vPPPPPPPPPPPPPPPPPP!w#W#f#k#W$V$l$xP%aPP%dP%{%{PPPP&PP#fPP&jP&v&y'SP'WP&j'^'d'k'q'z(Q(XPPP(_(c(w)Z)`*ZP*v*vP+XPP+aPPPPPP+e+ed`Od!Q!Y!f!i!n!q#W#XRsUiZOUd|!Q!Y!f!i!n!q#W#XVhPj!czWOPU]dgjkl!Q!Y!Z![!c!f!i!n!q!x#W#XR!^udROd!Q!Y!f!i!n!q#W#XQqUQ!VkR!WlQsUQ!P]Q!j![R#P!xd`Od!Q!Y!f!i!n!q#W#XUfPj!cQsUR!TgRoS{WOPU]dgjkl!Q!Y!Z![!c!f!i!n!q!x#W#XTwVydYOd!Q!Y!f!i!n!q#W#XgePU]gjkl!Z![!c!xe`Od!Q!Y!f!i!n!q#W#XR!m!fQ!t!nQ#Y#WR#Z#XT!y!t!zQ!}!tR#S!zQdOR!SdSjP!cR!UjQnSR!XnW!q!i!n#W#XR!v!qQyVR!`yS}[tR!e}Q!z!tR#Q!zTcOdSaOdQ!g!QQ!h!YQ!l!fZ!p!i!n!q#W#Xd[Od!Q!Y!f!i!n!q#W#XQtUR!d|ViPj!cdQOd!Q!Y!f!i!n!q#W#XUfPj!cQpUQ!O]Q!TgQ!VkQ!WlQ!j!ZQ!k![R#O!xdYOd!Q!Y!f!i!n!q#W#XdeP]gjkl!Z![!c!xRrUoTOPUdgj!Q!Y!c!f!i!n!q#W#XQ!r!iV!s!n#W#XTxVye^Od!Q!Y!f!i!n!q#W#X",
nodeNames: "⚠ Star Slash Plus Minus And Or Eq Neq Lt Lte Gt Gte Identifier AssignableIdentifier Word IdentifierBeforeDot Program PipeExpr FunctionCall PositionalArg ParenExpr FunctionCallOrIdentifier BinOp ConditionalOp FunctionDef keyword Params colon keyword String StringFragment Interpolation EscapeSeq Number Boolean Regex Null DotGet Underscore NamedArg NamedArgPrefix operator IfExpr keyword ThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
maxTerm: 84,
states: "/SQYQbOOO!TOpO'#CqO#aQcO'#CtO$ZOSO'#CvO%aQcO'#DsOOQa'#Ds'#DsO&gQcO'#DrO'OQRO'#CuO'^QcO'#DnO'uQbO'#D{OOQ`'#DO'#DOO'}QbO'#CsOOQ`'#Do'#DoO(oQbO'#DnO(}QbO'#EROOQ`'#DX'#DXO)lQRO'#DaOOQ`'#Dn'#DnO)qQQO'#DmOOQ`'#Dm'#DmOOQ`'#Db'#DbQYQbOOO)yObO,59]OOQa'#Dr'#DrOOQ`'#DS'#DSO*RQbO'#DUOOQ`'#EQ'#EQOOQ`'#Df'#DfO*]QbO,59[O*pQbO'#CxO*xQWO'#CyOOOO'#Du'#DuOOOO'#Dc'#DcO+^OSO,59bOOQa,59b,59bO(}QbO,59aO(}QbO,59aOOQ`'#Dd'#DdO+lQbO'#DPO+tQQO,5:gO+yQRO,59_O-`QRO'#CuO-pQRO,59_O-|QQO,59_O.RQQO,59_O.ZQbO'#DgO.fQbO,59ZO.wQRO,5:mO/OQQO,5:mO/TQbO,59{OOQ`,5:X,5:XOOQ`-E7`-E7`OOQa1G.w1G.wOOQ`,59p,59pOOQ`-E7d-E7dOOOO,59d,59dOOOO,59e,59eOOOO-E7a-E7aOOQa1G.|1G.|OOQa1G.{1G.{O/_QcO1G.{OOQ`-E7b-E7bO/yQbO1G0ROOQa1G.y1G.yO(}QbO,59iO(}QbO,59iO!YQbO'#CtO$iQbO'#CpOOQ`,5:R,5:ROOQ`-E7e-E7eO0WQbO1G0XOOQ`1G/g1G/gO0eQbO7+%mO0jQbO7+%nOOQO1G/T1G/TO0zQRO1G/TOOQ`'#DZ'#DZO1UQbO7+%sO1ZQbO7+%tOOQ`<<IX<<IXOOQ`'#De'#DeO1qQQO'#DeO1vQbO'#EOO2^QbO<<IYOOQ`<<I_<<I_OOQ`'#D['#D[O2cQbO<<I`OOQ`,5:P,5:POOQ`-E7c-E7cOOQ`AN>tAN>tO(}QbO'#D]OOQ`'#Dh'#DhO2nQbOAN>zO2yQQO'#D_OOQ`AN>zAN>zO3OQbOAN>zO3TQRO,59wO3[QQO,59wOOQ`-E7f-E7fOOQ`G24fG24fO3aQbOG24fO3fQQO,59yO3kQQO1G/cOOQ`LD*QLD*QO0jQbO1G/eO1ZQbO7+$}OOQ`7+%P7+%POOQ`<<Hi<<Hi",
stateData: "3s~O!_OS!`OS~O]QO^`O_TO`POaXOfTOnTOoTOpTO|^O!eZO!hRO!qcO~O!dfO~O]gO_TO`POaXOfTOnTOoTOpTOwhOyiO!eZO!hROzhX!qhX!whX!shXuhX~OP!fXQ!fXR!fXS!fXT!fXU!fXV!fXW!fXX!fXY!fXZ!fX[!fX~P!YOkoO!hrO!jmO!knO~O]gO_TO`POaXOfTOnTOoTOpTOwhOyiO!eZO!hRO~OP!gXQ!gXR!gXS!gX!q!gX!w!gXT!gXU!gXV!gXW!gXX!gXY!gXZ!gX[!gX!s!gXu!gX~P$iOP!fXQ!fXR!fXS!fX!q!bX!w!bXu!bX~OPsOQsORtOStO~OPsOQsORtOStO!q!bX!w!bXu!bX~O]uOtsP~O]QO_TO`POaXOfTOnTOoTOpTO!eZO!hRO~Oz}O!q!bX!w!bXu!bX~O]gO_TO`POfTOnTOoTOpTO!eZO!hRO~OV!RO~O!q!SO!w!SO~O]!UOf!UO~OaXOw!VO~P(}Ozda!qda!wda!sdauda~P$iO]!XO!eZO~O!h!YO!j!YO!k!YO!l!YO!m!YO!n!YO~OkoO!h![O!jmO!knO~O]uOtsX~Ot!`O~O!s!aOP!fXQ!fXR!fXS!fXT!fXU!fXV!fXW!fXX!fXY!fXZ!fX[!fX~OT!cOU!cOV!bOW!bOX!bOY!bOZ!bO[!bO~OPsOQsORtOStO~P,tOPsOQsORtOStO!s!aO~Oz}O!s!aO~O]!dO`PO!eZO~Oz}O!qca!wca!scauca~Ot!hO~P,tOt!hO~O^`O|^O~P'}OPsOQsORiiSii!qii!wii!siiuii~O^`O|^O!q!kO~P'}O^`O|^O!q!pO~P'}Ou!qO~O^`O|^O!q!rOu!rP~P'}O!sqitqi~P,tOu!vO~O^`O|^O!q!rOu!rP!Q!rP!S!rP~P'}O!q!yO~O^`O|^O!q!rOu!rX!Q!rX!S!rX~P'}Ou!{O~Ou#QO!Q!|O!S#PO~Ou#VO!Q!|O!S#PO~Ot#XO~Ou#VO~Ot#YO~P,tOt#YO~Ou#ZO~O!q#[O~O!q#]O~Ofo~",
goto: ",`!wPPPPPPPPPPPPPPPPPPP!x#X#gP$V#X$x%_P%x%xPPP%|&Y&sPP&vP&vPP&}P'Z'^'gP'kP&}'q'w'}(T(^(g(nPPPP(t(x)^PP)p*mP+[PPPPP+`+`P+sP+{,S,SdaOe!R!`!h!k!p!t#[#]R{Zi[OZe}!R!`!h!k!p!t#[#]fQOZe!R!`!h!k!p!t#[#]hgQS^ilst!b!c!d!e!|R!d}fSOZe!R!`!h!k!p!t#[#]hTQS^ilst!b!c!d!e!|Q!XmR!e}dWOe!R!`!h!k!p!t#[#]QzZQ!]sR!^t!PTOQSZ^eilst!R!`!b!c!d!e!h!k!p!t!|#[#]ToRqQ{ZQ!Q^Q!l!cR#T!|daOe!R!`!h!k!p!t#[#]YhQSl!d!eQ{ZR!ViRwXZjQSl!d!eeaOe!R!`!h!k!p!t#[#]R!o!hQ!x!pQ#^#[R#_#]T!}!x#OQ#R!xR#W#OQeOR!TeQqRR!ZqQvXR!_vW!t!k!p#[#]R!z!tWlQS!d!eR!WlS!O]|R!g!OQ#O!xR#U#OTdOeSbOeQ!i!RQ!j!`Q!n!hZ!s!k!p!t#[#]d]Oe!R!`!h!k!p!t#[#]Q|ZR!f}dVOe!R!`!h!k!p!t#[#]YhQSl!d!eQyZQ!P^Q!ViQ!]sQ!^tQ!l!bQ!m!cR#S!|dUOe!R!`!h!k!p!t#[#]hgQS^ilst!b!c!d!e!|RxZTpRqsYOQSZeil!R!`!d!e!h!k!p!t#[#]Q!u!kV!w!p#[#]ZkQSl!d!ee_Oe!R!`!h!k!p!t#[#]",
nodeNames: "⚠ Star Slash Plus Minus And Or Eq Neq Lt Lte Gt Gte Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Null ConditionalOp FunctionDef Params colon keyword PositionalArg Underscore NamedArg NamedArgPrefix operator IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
maxTerm: 85,
context: trackScope,
nodeProps: [
["closedBy", 28,"end"]
["closedBy", 36,"end"]
],
propSources: [highlighting],
skippedNodes: [0],
repeatNodeCount: 7,
tokenData: "<}~RyOX#rXY$aYZ$zZp#rpq$aqt#rtu%euw#rwx%jxy%oyz&Yz{#r{|&s|}#r}!O&s!O!P#r!P!Q)g!Q!['b![!]2S!]!^$z!^#O#r#O#P2m#P#R#r#R#S2r#S#T#r#T#Y3]#Y#Z4k#Z#b3]#b#c8y#c#f3]#f#g9p#g#h3]#h#i:g#i#o3]#o#p#r#p#q<_#q;'S#r;'S;=`$Z<%l~#r~O#r~~<xS#wUoSOt#ruw#rx#O#r#P;'S#r;'S;=`$Z<%lO#rS$^P;=`<%l#r^$hUoS!_YOt#ruw#rx#O#r#P;'S#r;'S;=`$Z<%lO#rU%RUoS!iQOt#ruw#rx#O#r#P;'S#r;'S;=`$Z<%lO#r~%jO!n~~%oO!l~U%vUoS!fQOt#ruw#rx#O#r#P;'S#r;'S;=`$Z<%lO#rU&aUoS!kQOt#ruw#rx#O#r#P;'S#r;'S;=`$Z<%lO#rU&xWoSOt#ruw#rx!Q#r!Q!['b![#O#r#P;'S#r;'S;=`$Z<%lO#rU'iYoSrQOt#ruw#rx!O#r!O!P(X!P!Q#r!Q!['b![#O#r#P;'S#r;'S;=`$Z<%lO#rU(^WoSOt#ruw#rx!Q#r!Q![(v![#O#r#P;'S#r;'S;=`$Z<%lO#rU(}WoSrQOt#ruw#rx!Q#r!Q![(v![#O#r#P;'S#r;'S;=`$Z<%lO#rU)lWoSOt#ruw#rx!P#r!P!Q*U!Q#O#r#P;'S#r;'S;=`$Z<%lO#rU*Z^oSOY+VYZ#rZt+Vtu,Yuw+Vwx,Yx!P+V!P!Q#r!Q!}+V!}#O0{#O#P.h#P;'S+V;'S;=`1|<%lO+VU+^^oStQOY+VYZ#rZt+Vtu,Yuw+Vwx,Yx!P+V!P!Q.}!Q!}+V!}#O0{#O#P.h#P;'S+V;'S;=`1|<%lO+VQ,_XtQOY,YZ!P,Y!P!Q,z!Q!},Y!}#O-i#O#P.h#P;'S,Y;'S;=`.w<%lO,YQ,}P!P!Q-QQ-VUtQ#Z#[-Q#]#^-Q#a#b-Q#g#h-Q#i#j-Q#m#n-QQ-lVOY-iZ#O-i#O#P.R#P#Q,Y#Q;'S-i;'S;=`.b<%lO-iQ.USOY-iZ;'S-i;'S;=`.b<%lO-iQ.eP;=`<%l-iQ.kSOY,YZ;'S,Y;'S;=`.w<%lO,YQ.zP;=`<%l,YU/SWoSOt#ruw#rx!P#r!P!Q/l!Q#O#r#P;'S#r;'S;=`$Z<%lO#rU/sboStQOt#ruw#rx#O#r#P#Z#r#Z#[/l#[#]#r#]#^/l#^#a#r#a#b/l#b#g#r#g#h/l#h#i#r#i#j/l#j#m#r#m#n/l#n;'S#r;'S;=`$Z<%lO#rU1Q[oSOY0{YZ#rZt0{tu-iuw0{wx-ix#O0{#O#P.R#P#Q+V#Q;'S0{;'S;=`1v<%lO0{U1yP;=`<%l0{U2PP;=`<%l+VU2ZUoSlQOt#ruw#rx#O#r#P;'S#r;'S;=`$Z<%lO#r~2rO!o~U2yUoSwQOt#ruw#rx#O#r#P;'S#r;'S;=`$Z<%lO#rU3bYoSOt#ruw#rx!_#r!_!`4Q!`#O#r#P#T#r#T#o3]#o;'S#r;'S;=`$Z<%lO#rU4XUyQoSOt#ruw#rx#O#r#P;'S#r;'S;=`$Z<%lO#rU4pZoSOt#ruw#rx!_#r!_!`4Q!`#O#r#P#T#r#T#U5c#U#o3]#o;'S#r;'S;=`$Z<%lO#rU5h[oSOt#ruw#rx!_#r!_!`4Q!`#O#r#P#T#r#T#`3]#`#a6^#a#o3]#o;'S#r;'S;=`$Z<%lO#rU6c[oSOt#ruw#rx!_#r!_!`4Q!`#O#r#P#T#r#T#g3]#g#h7X#h#o3]#o;'S#r;'S;=`$Z<%lO#rU7^[oSOt#ruw#rx!_#r!_!`4Q!`#O#r#P#T#r#T#X3]#X#Y8S#Y#o3]#o;'S#r;'S;=`$Z<%lO#rU8ZYsQoSOt#ruw#rx!_#r!_!`4Q!`#O#r#P#T#r#T#o3]#o;'S#r;'S;=`$Z<%lO#r^9QY!pWoSOt#ruw#rx!_#r!_!`4Q!`#O#r#P#T#r#T#o3]#o;'S#r;'S;=`$Z<%lO#r^9wY!rWoSOt#ruw#rx!_#r!_!`4Q!`#O#r#P#T#r#T#o3]#o;'S#r;'S;=`$Z<%lO#r^:n[!qWoSOt#ruw#rx!_#r!_!`4Q!`#O#r#P#T#r#T#f3]#f#g;d#g#o3]#o;'S#r;'S;=`$Z<%lO#rU;i[oSOt#ruw#rx!_#r!_!`4Q!`#O#r#P#T#r#T#i3]#i#j7X#j#o3]#o;'S#r;'S;=`$Z<%lO#rU<fUzQoSOt#ruw#rx#O#r#P;'S#r;'S;=`$Z<%lO#r~<}O!v~",
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!s~~", 11)],
topRules: {"Program":[0,17]},
specialized: [{term: 13, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
tokenPrec: 753
tokenData: ">i~RzOX#uXY$dYZ$}Zp#upq$dqs#ust%htu'Puw#uwx'Uxy'Zyz'tz{#u{|(_|}#u}!O(_!O!P#u!P!Q+R!Q![(|![!]3n!]!^$}!^#O#u#O#P4X#P#R#u#R#S4^#S#T#u#T#Y4w#Y#Z6V#Z#b4w#b#c:e#c#f4w#f#g;[#g#h4w#h#i<R#i#o4w#o#p#u#p#q=y#q;'S#u;'S;=`$^<%l~#u~O#u~~>dS#zUkSOt#uuw#ux#O#u#P;'S#u;'S;=`$^<%lO#uS$aP;=`<%l#u^$kUkS!_YOt#uuw#ux#O#u#P;'S#u;'S;=`$^<%lO#uU%UUkS!qQOt#uuw#ux#O#u#P;'S#u;'S;=`$^<%lO#u^%oZkS!`YOY%hYZ#uZt%htu&buw%hwx&bx#O%h#O#P&b#P;'S%h;'S;=`&y<%lO%hY&gS!`YOY&bZ;'S&b;'S;=`&s<%lO&bY&vP;=`<%l&b^&|P;=`<%l%h~'UO!j~~'ZO!h~U'bUkS!eQOt#uuw#ux#O#u#P;'S#u;'S;=`$^<%lO#uU'{UkS!sQOt#uuw#ux#O#u#P;'S#u;'S;=`$^<%lO#uU(dWkSOt#uuw#ux!Q#u!Q![(|![#O#u#P;'S#u;'S;=`$^<%lO#uU)TYkSfQOt#uuw#ux!O#u!O!P)s!P!Q#u!Q![(|![#O#u#P;'S#u;'S;=`$^<%lO#uU)xWkSOt#uuw#ux!Q#u!Q![*b![#O#u#P;'S#u;'S;=`$^<%lO#uU*iWkSfQOt#uuw#ux!Q#u!Q![*b![#O#u#P;'S#u;'S;=`$^<%lO#uU+WWkSOt#uuw#ux!P#u!P!Q+p!Q#O#u#P;'S#u;'S;=`$^<%lO#uU+u^kSOY,qYZ#uZt,qtu-tuw,qwx-tx!P,q!P!Q#u!Q!},q!}#O2g#O#P0S#P;'S,q;'S;=`3h<%lO,qU,x^kSoQOY,qYZ#uZt,qtu-tuw,qwx-tx!P,q!P!Q0i!Q!},q!}#O2g#O#P0S#P;'S,q;'S;=`3h<%lO,qQ-yXoQOY-tZ!P-t!P!Q.f!Q!}-t!}#O/T#O#P0S#P;'S-t;'S;=`0c<%lO-tQ.iP!P!Q.lQ.qUoQ#Z#[.l#]#^.l#a#b.l#g#h.l#i#j.l#m#n.lQ/WVOY/TZ#O/T#O#P/m#P#Q-t#Q;'S/T;'S;=`/|<%lO/TQ/pSOY/TZ;'S/T;'S;=`/|<%lO/TQ0PP;=`<%l/TQ0VSOY-tZ;'S-t;'S;=`0c<%lO-tQ0fP;=`<%l-tU0nWkSOt#uuw#ux!P#u!P!Q1W!Q#O#u#P;'S#u;'S;=`$^<%lO#uU1_bkSoQOt#uuw#ux#O#u#P#Z#u#Z#[1W#[#]#u#]#^1W#^#a#u#a#b1W#b#g#u#g#h1W#h#i#u#i#j1W#j#m#u#m#n1W#n;'S#u;'S;=`$^<%lO#uU2l[kSOY2gYZ#uZt2gtu/Tuw2gwx/Tx#O2g#O#P/m#P#Q,q#Q;'S2g;'S;=`3b<%lO2gU3eP;=`<%l2gU3kP;=`<%l,qU3uUkStQOt#uuw#ux#O#u#P;'S#u;'S;=`$^<%lO#u~4^O!k~U4eUkSwQOt#uuw#ux#O#u#P;'S#u;'S;=`$^<%lO#uU4|YkSOt#uuw#ux!_#u!_!`5l!`#O#u#P#T#u#T#o4w#o;'S#u;'S;=`$^<%lO#uU5sUyQkSOt#uuw#ux#O#u#P;'S#u;'S;=`$^<%lO#uU6[ZkSOt#uuw#ux!_#u!_!`5l!`#O#u#P#T#u#T#U6}#U#o4w#o;'S#u;'S;=`$^<%lO#uU7S[kSOt#uuw#ux!_#u!_!`5l!`#O#u#P#T#u#T#`4w#`#a7x#a#o4w#o;'S#u;'S;=`$^<%lO#uU7}[kSOt#uuw#ux!_#u!_!`5l!`#O#u#P#T#u#T#g4w#g#h8s#h#o4w#o;'S#u;'S;=`$^<%lO#uU8x[kSOt#uuw#ux!_#u!_!`5l!`#O#u#P#T#u#T#X4w#X#Y9n#Y#o4w#o;'S#u;'S;=`$^<%lO#uU9uYnQkSOt#uuw#ux!_#u!_!`5l!`#O#u#P#T#u#T#o4w#o;'S#u;'S;=`$^<%lO#u^:lY!lWkSOt#uuw#ux!_#u!_!`5l!`#O#u#P#T#u#T#o4w#o;'S#u;'S;=`$^<%lO#u^;cY!nWkSOt#uuw#ux!_#u!_!`5l!`#O#u#P#T#u#T#o4w#o;'S#u;'S;=`$^<%lO#u^<Y[!mWkSOt#uuw#ux!_#u!_!`5l!`#O#u#P#T#u#T#f4w#f#g=O#g#o4w#o;'S#u;'S;=`$^<%lO#uU=T[kSOt#uuw#ux!_#u!_!`5l!`#O#u#P#T#u#T#i4w#i#j8s#j#o4w#o;'S#u;'S;=`$^<%lO#uU>QUzQkSOt#uuw#ux#O#u#P;'S#u;'S;=`$^<%lO#u~>iO!w~",
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!d~~", 11)],
topRules: {"Program":[0,18]},
specialized: [{term: 13, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 13, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
tokenPrec: 860
})

View File

@ -30,6 +30,204 @@ describe('Identifier', () => {
FunctionCallOrIdentifier
Identifier moo-😊-34`)
})
test('parses mathematical unicode symbols like 𝜋 as identifiers', () => {
expect('𝜋').toMatchTree(`
FunctionCallOrIdentifier
Identifier 𝜋`)
})
})
describe('Unicode Symbol Support', () => {
describe('Emoji (currently supported)', () => {
test('Basic Emoticons (U+1F600-U+1F64F)', () => {
expect('😀').toMatchTree(`
FunctionCallOrIdentifier
Identifier 😀`)
expect('😊-counter').toMatchTree(`
FunctionCallOrIdentifier
Identifier 😊-counter`)
})
test('Miscellaneous Symbols and Pictographs (U+1F300-U+1F5FF)', () => {
expect('🌍').toMatchTree(`
FunctionCallOrIdentifier
Identifier 🌍`)
expect('🔥-handler').toMatchTree(`
FunctionCallOrIdentifier
Identifier 🔥-handler`)
})
test('Transport and Map Symbols (U+1F680-U+1F6FF)', () => {
expect('🚀').toMatchTree(`
FunctionCallOrIdentifier
Identifier 🚀`)
expect('🚀-launch').toMatchTree(`
FunctionCallOrIdentifier
Identifier 🚀-launch`)
})
test('Regional Indicator Symbols / Flags (U+1F1E6-U+1F1FF)', () => {
// Note: Flags are typically two regional indicators combined
expect('🇺').toMatchTree(`
FunctionCallOrIdentifier
Identifier 🇺`)
})
test('Supplemental Symbols and Pictographs (U+1F900-U+1F9FF)', () => {
expect('🤖').toMatchTree(`
FunctionCallOrIdentifier
Identifier 🤖`)
expect('🦀-lang').toMatchTree(`
FunctionCallOrIdentifier
Identifier 🦀-lang`)
})
test('Dingbats (U+2700-U+27BF)', () => {
expect('✂').toMatchTree(`
FunctionCallOrIdentifier
Identifier `)
expect('✨-magic').toMatchTree(`
FunctionCallOrIdentifier
Identifier -magic`)
})
test('Miscellaneous Symbols (U+2600-U+26FF)', () => {
expect('⚡').toMatchTree(`
FunctionCallOrIdentifier
Identifier `)
expect('☀-bright').toMatchTree(`
FunctionCallOrIdentifier
Identifier -bright`)
})
})
describe('Greek Letters (not currently supported)', () => {
test('Greek lowercase alpha α (U+03B1)', () => {
expect('α').toMatchTree(`
FunctionCallOrIdentifier
Identifier α`)
})
test('Greek lowercase beta β (U+03B2)', () => {
expect('β').toMatchTree(`
FunctionCallOrIdentifier
Identifier β`)
})
test('Greek lowercase lambda λ (U+03BB)', () => {
expect('λ').toMatchTree(`
FunctionCallOrIdentifier
Identifier λ`)
})
test('Greek lowercase pi π (U+03C0)', () => {
// Note: This is different from mathematical pi 𝜋
expect('π').toMatchTree(`
FunctionCallOrIdentifier
Identifier π`)
})
})
describe('Mathematical Alphanumeric Symbols (not currently supported)', () => {
test('Mathematical italic small pi 𝜋 (U+1D70B)', () => {
expect('𝜋').toMatchTree(`
FunctionCallOrIdentifier
Identifier 𝜋`)
})
test('Mathematical bold small x 𝐱 (U+1D431)', () => {
expect('𝐱').toMatchTree(`
FunctionCallOrIdentifier
Identifier 𝐱`)
})
test('Mathematical script capital F 𝓕 (U+1D4D5)', () => {
expect('𝓕').toMatchTree(`
FunctionCallOrIdentifier
Identifier 𝓕`)
})
})
describe('Mathematical Operators (not currently supported)', () => {
test('Infinity symbol ∞ (U+221E)', () => {
expect('∞').toMatchTree(`
FunctionCallOrIdentifier
Identifier `)
})
test('Sum symbol ∑ (U+2211)', () => {
expect('∑').toMatchTree(`
FunctionCallOrIdentifier
Identifier `)
})
test('Integral symbol ∫ (U+222B)', () => {
expect('∫').toMatchTree(`
FunctionCallOrIdentifier
Identifier `)
})
})
describe('Superscripts and Subscripts (not currently supported)', () => {
test('Superscript two ² (U+00B2)', () => {
expect('x²').toMatchTree(`
FunctionCallOrIdentifier
Identifier x²`)
})
test('Subscript two ₂ (U+2082)', () => {
expect('h₂o').toMatchTree(`
FunctionCallOrIdentifier
Identifier ho`)
})
})
describe('Arrows (not currently supported)', () => {
test('Rightward arrow → (U+2192)', () => {
expect('→').toMatchTree(`
FunctionCallOrIdentifier
Identifier `)
})
test('Leftward arrow ← (U+2190)', () => {
expect('←').toMatchTree(`
FunctionCallOrIdentifier
Identifier `)
})
test('Double rightward arrow ⇒ (U+21D2)', () => {
expect('⇒').toMatchTree(`
FunctionCallOrIdentifier
Identifier `)
})
})
describe('CJK Symbols (not currently supported)', () => {
test('Hiragana あ (U+3042)', () => {
expect('あ').toMatchTree(`
FunctionCallOrIdentifier
Identifier `)
})
test('Katakana カ (U+30AB)', () => {
expect('カ').toMatchTree(`
FunctionCallOrIdentifier
Identifier `)
})
test('CJK Unified Ideograph 中 (U+4E2D)', () => {
expect('中').toMatchTree(`
FunctionCallOrIdentifier
Identifier `)
})
})
})
describe('Parentheses', () => {
@ -299,10 +497,10 @@ describe('Assign', () => {
AssignableIdentifier add
Eq =
FunctionDef
keyword do
Do do
Params
AssignableIdentifier a
AssignableIdentifier b
Identifier a
Identifier b
colon :
BinOp
Identifier a
@ -319,6 +517,7 @@ describe('DotGet whitespace sensitivity', () => {
AssignableIdentifier basename
Eq =
Number 5
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot basename
Identifier prop`)
@ -348,3 +547,27 @@ describe('DotGet whitespace sensitivity', () => {
expect('readme.txt').toMatchTree(`Word readme.txt`)
})
})
describe('Comments', () => {
test('are barely there', () => {
expect(`x = 5 # one banana\ny = 2 # two bananas`).toMatchTree(`
Assign
AssignableIdentifier x
Eq =
Number 5
Assign
AssignableIdentifier y
Eq =
Number 2`)
expect('# some comment\nbasename = 5 # very astute\n basename / prop\n# good info').toMatchTree(`
Assign
AssignableIdentifier basename
Eq =
Number 5
BinOp
Identifier basename
Slash /
Identifier prop`)
})
})

View File

@ -4,7 +4,7 @@ import '../shrimp.grammar' // Importing this so changes cause it to retest!
describe('if/elseif/else', () => {
test('parses single line if', () => {
expect(`if y = 1: 'cool'`).toMatchTree(`
expect(`if y = 1: 'cool' end`).toMatchTree(`
IfExpr
keyword if
ConditionalOp
@ -12,12 +12,13 @@ describe('if/elseif/else', () => {
Eq =
Number 1
colon :
ThenBlock
SingleLineThenBlock
String
StringFragment cool
keyword end
`)
expect('a = if x: 2').toMatchTree(`
expect('a = if x: 2 end').toMatchTree(`
Assign
AssignableIdentifier a
Eq =
@ -25,8 +26,9 @@ describe('if/elseif/else', () => {
keyword if
Identifier x
colon :
ThenBlock
SingleLineThenBlock
Number 2
keyword end
`)
})
@ -138,7 +140,7 @@ describe('if/elseif/else', () => {
})
test('does not parse identifiers that start with if', () => {
expect('iffy = if true: 2').toMatchTree(`
expect('iffy = if true: 2 end').toMatchTree(`
Assign
AssignableIdentifier iffy
Eq =
@ -146,8 +148,9 @@ describe('if/elseif/else', () => {
keyword if
Boolean true
colon :
ThenBlock
SingleLineThenBlock
Number 2
keyword end
`)
})
})

View File

@ -20,6 +20,7 @@ describe('DotGet', () => {
AssignableIdentifier obj
Eq =
Number 5
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot obj
Identifier prop
@ -29,10 +30,11 @@ describe('DotGet', () => {
test('function parameters are in scope within function body', () => {
expect('do config: config.path end').toMatchTree(`
FunctionDef
keyword do
Do do
Params
AssignableIdentifier config
Identifier config
colon :
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot config
Identifier path
@ -43,10 +45,11 @@ describe('DotGet', () => {
test('parameters out of scope outside function', () => {
expect('do x: x.prop end; x.prop').toMatchTree(`
FunctionDef
keyword do
Do do
Params
AssignableIdentifier x
Identifier x
colon :
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot x
Identifier prop
@ -61,14 +64,16 @@ describe('DotGet', () => {
y.bar
end`).toMatchTree(`
FunctionDef
keyword do
Do do
Params
AssignableIdentifier x
AssignableIdentifier y
Identifier x
Identifier y
colon :
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot x
Identifier foo
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot y
Identifier bar
@ -82,18 +87,20 @@ end`).toMatchTree(`
do y: y.inner end
end`).toMatchTree(`
FunctionDef
keyword do
Do do
Params
AssignableIdentifier x
Identifier x
colon :
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot x
Identifier outer
FunctionDef
keyword do
Do do
Params
AssignableIdentifier y
Identifier y
colon :
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot y
Identifier inner
@ -117,6 +124,62 @@ end`).toMatchTree(`
`)
})
test('dot get works as bare function', () => {
expect('io = dict print=echo; io.print').toMatchTree(`
Assign
AssignableIdentifier io
Eq =
FunctionCall
Identifier dict
NamedArg
NamedArgPrefix print=
Identifier echo
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot io
Identifier print
`)
})
test('dot get works as function w/ args', () => {
expect('io = dict print=echo; io.print heya').toMatchTree(`
Assign
AssignableIdentifier io
Eq =
FunctionCall
Identifier dict
NamedArg
NamedArgPrefix print=
Identifier echo
FunctionCall
DotGet
IdentifierBeforeDot io
Identifier print
PositionalArg
Identifier heya
`)
})
test('dot get works as function in parens', () => {
expect('io = dict print=echo; (io.print heya)').toMatchTree(`
Assign
AssignableIdentifier io
Eq =
FunctionCall
Identifier dict
NamedArg
NamedArgPrefix print=
Identifier echo
ParenExpr
FunctionCall
DotGet
IdentifierBeforeDot io
Identifier print
PositionalArg
Identifier heya
`)
})
test('mixed file paths and dot get', () => {
expect('config = 42; cat readme.txt; echo config.path').toMatchTree(`
Assign
@ -145,4 +208,70 @@ end`).toMatchTree(`
PositionalArg
Identifier prop`)
})
test('readme.1 is Word when readme not in scope', () => {
expect('readme.1').toMatchTree(`Word readme.1`)
})
test('readme.1 is Word when used in function', () => {
expect('echo readme.1').toMatchTree(`
FunctionCall
Identifier echo
PositionalArg
Word readme.1`)
})
test('obj.1 is DotGet when obj is assigned', () => {
expect('obj = 5; obj.1').toMatchTree(`
Assign
AssignableIdentifier obj
Eq =
Number 5
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot obj
Number 1
`)
})
test('obj.1 arg is DotGet when obj is assigned', () => {
expect('obj = 5; obj.1').toMatchTree(`
Assign
AssignableIdentifier obj
Eq =
Number 5
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot obj
Number 1
`)
})
test('dot get index works as function w/ args', () => {
expect(`io = list (do x: echo x end); io.0 heya`).toMatchTree(`
Assign
AssignableIdentifier io
Eq =
FunctionCall
Identifier list
PositionalArg
ParenExpr
FunctionDef
Do do
Params
Identifier x
colon :
FunctionCall
Identifier echo
PositionalArg
Identifier x
keyword end
FunctionCall
DotGet
IdentifierBeforeDot io
Number 0
PositionalArg
Identifier heya
`)
})
})

View File

@ -60,7 +60,7 @@ describe('Do', () => {
test('parses function no parameters', () => {
expect('do: 1 end').toMatchTree(`
FunctionDef
keyword do
Do do
Params
colon :
Number 1
@ -70,9 +70,9 @@ describe('Do', () => {
test('parses function with single parameter', () => {
expect('do x: x + 1 end').toMatchTree(`
FunctionDef
keyword do
Do do
Params
AssignableIdentifier x
Identifier x
colon :
BinOp
Identifier x
@ -84,10 +84,10 @@ describe('Do', () => {
test('parses function with multiple parameters', () => {
expect('do x y: x * y end').toMatchTree(`
FunctionDef
keyword do
Do do
Params
AssignableIdentifier x
AssignableIdentifier y
Identifier x
Identifier y
colon :
BinOp
Identifier x
@ -102,10 +102,10 @@ describe('Do', () => {
x + 9
end`).toMatchTree(`
FunctionDef
keyword do
Do do
Params
AssignableIdentifier x
AssignableIdentifier y
Identifier x
Identifier y
colon :
BinOp
Identifier x
@ -124,9 +124,9 @@ end`).toMatchTree(`
AssignableIdentifier fnnn
Eq =
FunctionDef
keyword do
Do do
Params
AssignableIdentifier x
Identifier x
colon :
FunctionCallOrIdentifier
Identifier x
@ -139,12 +139,29 @@ end`).toMatchTree(`
AssignableIdentifier enddd
Eq =
FunctionDef
keyword do
Do do
Params
AssignableIdentifier x
Identifier x
colon :
FunctionCallOrIdentifier
Identifier x
keyword end`)
})
test('can call a function returned by a parens expression', () => {
expect('(do x: x end) 5').toMatchTree(`
FunctionCall
ParenExpr
FunctionDef
Do do
Params
Identifier x
colon :
FunctionCallOrIdentifier
Identifier x
keyword end
PositionalArg
Number 5
`)
})
})

View File

@ -24,10 +24,10 @@ describe('multiline', () => {
AssignableIdentifier add
Eq =
FunctionDef
keyword do
Do do
Params
AssignableIdentifier a
AssignableIdentifier b
Identifier a
Identifier b
colon :
Assign
AssignableIdentifier result
@ -61,14 +61,30 @@ end
Number 3
FunctionDef
keyword do
Do do
Params
AssignableIdentifier x
AssignableIdentifier y
Identifier x
Identifier y
colon :
FunctionCallOrIdentifier
Identifier x
keyword end
`)
})
test('multiline with empty lines', () => {
expect(`
do:
2
end
`).toMatchTree(`
FunctionDef
Do do
Params
colon :
Number 2
keyword end
`)
})
})

View File

@ -75,13 +75,27 @@ describe('pipe expressions', () => {
Identifier each
PositionalArg
FunctionDef
keyword do
Do do
Params
AssignableIdentifier x
Identifier x
colon :
FunctionCallOrIdentifier
Identifier x
keyword end
`)
})
test(`double trouble (do keyword isn't over eager)`, () => {
expect(`
double 2 | double`).toMatchTree(`
PipeExpr
FunctionCall
Identifier double
PositionalArg
Number 2
operator |
FunctionCallOrIdentifier
Identifier double
`)
})
})

View File

@ -1,5 +1,10 @@
import { ExternalTokenizer, InputStream, Stack } from '@lezer/lr'
import { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot } from './shrimp.terms'
import { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot, Do } from './shrimp.terms'
// doobie doobie do (we need the `do` keyword to know when we're defining params)
export function specializeKeyword(ident: string) {
return ident === 'do' ? Do : -1
}
// The only chars that can't be words are whitespace, apostrophes, closing parens, and EOF.
@ -14,7 +19,7 @@ export const tokenizer = new ExternalTokenizer(
// Don't consume things that start with - or + followed by a digit (negative/positive numbers)
if ((ch === 45 /* - */ || ch === 43) /* + */ && isDigit(input.peek(1))) return
const isValidStart = isLowercaseLetter(ch) || isEmoji(ch)
const isValidStart = isLowercaseLetter(ch) || isEmojiOrUnicode(ch)
const canBeWord = stack.canShift(Word)
// Consume all word characters, tracking if it remains a valid identifier
@ -106,8 +111,8 @@ const consumeWordToken = (
if (!isWordChar(nextCh)) break
}
// Track identifier validity: must be lowercase, digit, dash, or emoji
if (!isLowercaseLetter(ch) && !isDigit(ch) && ch !== 45 /* - */ && !isEmoji(ch)) {
// Track identifier validity: must be lowercase, digit, dash, or emoji/unicode
if (!isLowercaseLetter(ch) && !isDigit(ch) && ch !== 45 /* - */ && !isEmojiOrUnicode(ch)) {
if (!canBeWord) break
isValidIdentifier = false
}
@ -217,7 +222,7 @@ const getFullCodePoint = (input: InputStream, pos: number): number => {
return ch
}
const isEmoji = (ch: number): boolean => {
const isEmojiOrUnicode = (ch: number): boolean => {
return (
// Basic Emoticons
(ch >= 0x1f600 && ch <= 0x1f64f) ||
@ -242,7 +247,25 @@ const isEmoji = (ch: number): boolean => {
// Additional miscellaneous items
(ch >= 0x238c && ch <= 0x2454) ||
// Combining Diacritical Marks for Symbols
(ch >= 0x20d0 && ch <= 0x20ff)
(ch >= 0x20d0 && ch <= 0x20ff) ||
// Latin-1 Supplement (includes ², ³, ¹ and other special chars)
(ch >= 0x00a0 && ch <= 0x00ff) ||
// Greek and Coptic (U+0370-U+03FF)
(ch >= 0x0370 && ch <= 0x03ff) ||
// Mathematical Alphanumeric Symbols (U+1D400-U+1D7FF)
(ch >= 0x1d400 && ch <= 0x1d7ff) ||
// Mathematical Operators (U+2200-U+22FF)
(ch >= 0x2200 && ch <= 0x22ff) ||
// Superscripts and Subscripts (U+2070-U+209F)
(ch >= 0x2070 && ch <= 0x209f) ||
// Arrows (U+2190-U+21FF)
(ch >= 0x2190 && ch <= 0x21ff) ||
// Hiragana (U+3040-U+309F)
(ch >= 0x3040 && ch <= 0x309f) ||
// Katakana (U+30A0-U+30FF)
(ch >= 0x30a0 && ch <= 0x30ff) ||
// CJK Unified Ideographs (U+4E00-U+9FFF)
(ch >= 0x4e00 && ch <= 0x9fff)
)
}

141
src/prelude/index.ts Normal file
View File

@ -0,0 +1,141 @@
// The prelude creates all the builtin Shrimp functions.
import { resolve, parse } from 'path'
import { readFileSync } from 'fs'
import { Compiler } from '#compiler/compiler'
import {
VM, Scope, toValue, type Value,
extractParamInfo, isWrapped, getOriginalFunction,
} from 'reefvm'
export const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
cyan: '\x1b[36m',
yellow: '\x1b[33m',
green: '\x1b[32m',
red: '\x1b[31m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
pink: '\x1b[38;2;255;105;180m'
}
export const globalFunctions = {
// hello
echo: (...args: any[]) => {
console.log(...args.map(a => {
const v = toValue(a)
return ['array', 'dict'].includes(v.type) ? formatValue(v, true) : v.value
}))
return toValue(null)
},
// info
type: (v: any) => toValue(v).type,
inspect: (v: any) => formatValue(toValue(v)),
length: (v: any) => {
const value = toValue(v)
switch (value.type) {
case 'string': case 'array': return value.value.length
case 'dict': return value.value.size
default: return 0
}
},
// strings
join: (arr: string[], sep: string = ',') => arr.join(sep),
split: (str: string, sep: string = ',') => str.split(sep),
'to-upper': (str: string) => str.toUpperCase(),
'to-lower': (str: string) => str.toLowerCase(),
trim: (str: string) => str.trim(),
// collections
at: (collection: any, index: number | string) => collection[index],
list: (...args: any[]) => args,
dict: (atNamed = {}) => atNamed,
slice: (list: any[], start: number, end?: number) => list.slice(start, end),
range: (start: number, end: number | null) => {
if (end === null) {
end = start
start = 0
}
const result: number[] = []
for (let i = start; i <= end; i++) {
result.push(i)
}
return result
},
// enumerables
map: async (list: any[], cb: Function) => {
let acc: any[] = []
for (const value of list) acc.push(await cb(value))
return acc
},
each: async (list: any[], cb: Function) => {
for (const value of list) await cb(value)
},
// modules
use: async function (this: VM, path: string) {
const scope = this.scope
const pc = this.pc
const fullPath = resolve(path) + '.sh'
const code = readFileSync(fullPath, 'utf-8')
this.pc = this.instructions.length
this.scope = new Scope(scope)
const compiled = new Compiler(code)
this.appendBytecode(compiled.bytecode)
await this.continue()
const module: Map<string, Value> = new Map
for (const [name, value] of this.scope.locals.entries())
module.set(name, value)
this.scope = scope
this.pc = pc
this.stopped = false
this.scope.set(parse(fullPath).name, { type: 'dict', value: module })
}
}
export function formatValue(value: Value, inner = false): string {
switch (value.type) {
case 'string':
return `${colors.green}'${value.value.replaceAll("'", "\\'")}${colors.green}'${colors.reset}`
case 'number':
return `${colors.cyan}${value.value}${colors.reset}`
case 'boolean':
return `${colors.yellow}${value.value}${colors.reset}`
case 'null':
return `${colors.dim}null${colors.reset}`
case 'array': {
const items = value.value.map(x => formatValue(x, true)).join(' ')
return `${inner ? '(' : ''}${colors.blue}list${colors.reset} ${items}${inner ? ')' : ''}`
}
case 'dict': {
const entries = Array.from(value.value.entries())
.map(([k, v]) => `${k}=${formatValue(v, true)}`)
.join(' ')
return `${inner ? '(' : ''}${colors.magenta}dict${colors.reset} ${entries}${inner ? ')' : ''}`
}
case 'function': {
const params = value.params.length ? '(' + value.params.join(' ') + ')' : ''
return `${colors.dim}<function${params}>${colors.reset}`
}
case 'native':
const fn = isWrapped(value.fn) ? getOriginalFunction(value.fn) : value.fn
const info = extractParamInfo(fn)
const params = info.params.length ? '(' + info.params.join(' ') + ')' : ''
return `${colors.dim}<native${params}>${colors.reset}`
case 'regex':
return `${colors.magenta}${value.value}${colors.reset}`
default:
return String(value)
}
}

View File

@ -0,0 +1,4 @@
🥧 = 3.14159265359
pi = 3.14
add1 = do x: x + 1 end
double = do x: x * 2 end

View File

@ -0,0 +1,256 @@
import { expect, describe, test, mock } from 'bun:test'
import { globalFunctions, formatValue } from '#prelude'
import { toValue } from 'reefvm'
describe('string operations', () => {
test('to-upper converts to uppercase', () => {
expect(globalFunctions['to-upper']('hello')).toBe('HELLO')
expect(globalFunctions['to-upper']('Hello World!')).toBe('HELLO WORLD!')
})
test('to-lower converts to lowercase', () => {
expect(globalFunctions['to-lower']('HELLO')).toBe('hello')
expect(globalFunctions['to-lower']('Hello World!')).toBe('hello world!')
})
test('trim removes whitespace', () => {
expect(globalFunctions.trim(' hello ')).toBe('hello')
expect(globalFunctions.trim('\n\thello\t\n')).toBe('hello')
})
test('split divides string by separator', () => {
expect(globalFunctions.split('a,b,c', ',')).toEqual(['a', 'b', 'c'])
expect(globalFunctions.split('hello', '')).toEqual(['h', 'e', 'l', 'l', 'o'])
})
test('split uses comma as default separator', () => {
expect(globalFunctions.split('a,b,c')).toEqual(['a', 'b', 'c'])
})
test('join combines array elements', () => {
expect(globalFunctions.join(['a', 'b', 'c'], '-')).toBe('a-b-c')
expect(globalFunctions.join(['hello', 'world'], ' ')).toBe('hello world')
})
test('join uses comma as default separator', () => {
expect(globalFunctions.join(['a', 'b', 'c'])).toBe('a,b,c')
})
})
describe('introspection', () => {
test('type returns proper types', () => {
expect(globalFunctions.type(toValue('hello'))).toBe('string')
expect(globalFunctions.type('hello')).toBe('string')
expect(globalFunctions.type(toValue(42))).toBe('number')
expect(globalFunctions.type(42)).toBe('number')
expect(globalFunctions.type(toValue(true))).toBe('boolean')
expect(globalFunctions.type(false)).toBe('boolean')
expect(globalFunctions.type(toValue(null))).toBe('null')
expect(globalFunctions.type(toValue([1, 2, 3]))).toBe('array')
const dict = new Map([['key', toValue('value')]])
expect(globalFunctions.type({ type: 'dict', value: dict })).toBe('dict')
})
test('length', () => {
expect(globalFunctions.length(toValue('hello'))).toBe(5)
expect(globalFunctions.length('hello')).toBe(5)
expect(globalFunctions.length(toValue([1, 2, 3]))).toBe(3)
expect(globalFunctions.length([1, 2, 3])).toBe(3)
const dict = new Map([['a', toValue(1)], ['b', toValue(2)]])
expect(globalFunctions.length({ type: 'dict', value: dict })).toBe(2)
expect(globalFunctions.length(toValue(42))).toBe(0)
expect(globalFunctions.length(toValue(true))).toBe(0)
expect(globalFunctions.length(toValue(null))).toBe(0)
})
test('inspect formats values', () => {
const result = globalFunctions.inspect(toValue('hello'))
expect(result).toContain('hello')
})
})
describe('collections', () => {
test('list creates array from arguments', () => {
expect(globalFunctions.list(1, 2, 3)).toEqual([1, 2, 3])
expect(globalFunctions.list('a', 'b')).toEqual(['a', 'b'])
expect(globalFunctions.list()).toEqual([])
})
test('dict creates object from named arguments', () => {
expect(globalFunctions.dict({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 })
expect(globalFunctions.dict()).toEqual({})
})
test('at retrieves element at index', () => {
expect(globalFunctions.at([10, 20, 30], 0)).toBe(10)
expect(globalFunctions.at([10, 20, 30], 2)).toBe(30)
})
test('at retrieves property from object', () => {
expect(globalFunctions.at({ name: 'test' }, 'name')).toBe('test')
})
test('slice extracts array subset', () => {
expect(globalFunctions.slice([1, 2, 3, 4, 5], 1, 3)).toEqual([2, 3])
expect(globalFunctions.slice([1, 2, 3, 4, 5], 2)).toEqual([3, 4, 5])
})
test('range creates number sequence', () => {
expect(globalFunctions.range(0, 5)).toEqual([0, 1, 2, 3, 4, 5])
expect(globalFunctions.range(3, 6)).toEqual([3, 4, 5, 6])
})
test('range with single argument starts from 0', () => {
expect(globalFunctions.range(3, null)).toEqual([0, 1, 2, 3])
expect(globalFunctions.range(0, null)).toEqual([0])
})
})
describe('enumerables', () => {
test('map transforms array elements', async () => {
const double = (x: number) => x * 2
const result = await globalFunctions.map([1, 2, 3], double)
expect(result).toEqual([2, 4, 6])
})
test('map works with async callbacks', async () => {
const asyncDouble = async (x: number) => {
await Promise.resolve()
return x * 2
}
const result = await globalFunctions.map([1, 2, 3], asyncDouble)
expect(result).toEqual([2, 4, 6])
})
test('map handles empty array', async () => {
const fn = (x: number) => x * 2
const result = await globalFunctions.map([], fn)
expect(result).toEqual([])
})
test('each iterates over array', async () => {
const results: number[] = []
await globalFunctions.each([1, 2, 3], (x: number) => {
results.push(x * 2)
})
expect(results).toEqual([2, 4, 6])
})
test('each works with async callbacks', async () => {
const results: number[] = []
await globalFunctions.each([1, 2, 3], async (x: number) => {
await Promise.resolve()
results.push(x * 2)
})
expect(results).toEqual([2, 4, 6])
})
test('each handles empty array', async () => {
let called = false
await globalFunctions.each([], () => {
called = true
})
expect(called).toBe(false)
})
})
describe('echo', () => {
test('echo logs arguments to console', () => {
const spy = mock(() => { })
const originalLog = console.log
console.log = spy
globalFunctions.echo('hello', 'world')
expect(spy).toHaveBeenCalledWith('hello', 'world')
console.log = originalLog
})
test('echo returns null value', () => {
const originalLog = console.log
console.log = () => { }
const result = globalFunctions.echo('test')
expect(result).toEqual(toValue(null))
console.log = originalLog
})
test('echo formats array values', () => {
const spy = mock(() => { })
const originalLog = console.log
console.log = spy
globalFunctions.echo(toValue([1, 2, 3]))
// Should format the array, not just log the raw value
expect(spy).toHaveBeenCalled()
// @ts-ignore
const logged = spy.mock.calls[0][0]
// @ts-ignore
expect(logged).toContain('list')
console.log = originalLog
})
})
describe('formatValue', () => {
test('formats string with quotes', () => {
const result = formatValue(toValue('hello'))
expect(result).toContain('hello')
expect(result).toContain("'")
})
test('formats numbers', () => {
const result = formatValue(toValue(42))
expect(result).toContain('42')
})
test('formats booleans', () => {
expect(formatValue(toValue(true))).toContain('true')
expect(formatValue(toValue(false))).toContain('false')
})
test('formats null', () => {
const result = formatValue(toValue(null))
expect(result).toContain('null')
})
test('formats arrays', () => {
const result = formatValue(toValue([1, 2, 3]))
expect(result).toContain('list')
})
test('formats nested arrays with parentheses', () => {
const inner = toValue([1, 2])
const outer = toValue([inner])
const result = formatValue(outer)
expect(result).toContain('list')
expect(result).toContain('(')
expect(result).toContain(')')
})
test('formats dicts', () => {
const dict = new Map([
['name', toValue('test')],
['age', toValue(42)]
])
const result = formatValue({ type: 'dict', value: dict })
expect(result).toContain('dict')
expect(result).toContain('name=')
expect(result).toContain('age=')
})
test('escapes single quotes in strings', () => {
const result = formatValue(toValue("it's"))
expect(result).toContain("\\'")
})
})

View File

@ -0,0 +1,28 @@
import { expect, describe, test } from 'bun:test'
import { globalFunctions } from '#prelude'
describe('use', () => {
test(`imports all a file's functions`, async () => {
expect(`
use ./src/prelude/tests/math
dbl = math | at double
dbl 4
`).toEvaluateTo(8, globalFunctions)
expect(`
use ./src/prelude/tests/math
math | at pi
`).toEvaluateTo(3.14, globalFunctions)
expect(`
use ./src/prelude/tests/math
math | at 🥧
`).toEvaluateTo(3.14159265359, globalFunctions)
expect(`
use ./src/prelude/tests/math
call = do x y: x y end
call (math | at add1) 5
`).toEvaluateTo(6, globalFunctions)
})
})

View File

@ -3,7 +3,7 @@ import { parser } from '#parser/shrimp'
import { $ } from 'bun'
import { assert, errorMessage } from '#utils/utils'
import { Compiler } from '#compiler/compiler'
import { run, VM } from 'reefvm'
import { run, VM, type TypeScriptFunction } from 'reefvm'
import { treeToString, VMResultToValue } from '#utils/tree'
const regenerateParser = async () => {
@ -33,7 +33,7 @@ declare module 'bun:test' {
toMatchTree(expected: string): T
toMatchExpression(expected: string): T
toFailParse(): T
toEvaluateTo(expected: unknown, nativeFunctions?: Record<string, Function>): Promise<T>
toEvaluateTo(expected: unknown, globalFunctions?: Record<string, TypeScriptFunction>): Promise<T>
toFailEvaluation(): Promise<T>
}
}
@ -96,13 +96,13 @@ expect.extend({
async toEvaluateTo(
received: unknown,
expected: unknown,
nativeFunctions: Record<string, Function> = {}
globalFunctions: Record<string, TypeScriptFunction> = {}
) {
assert(typeof received === 'string', 'toEvaluateTo can only be used with string values')
try {
const compiler = new Compiler(received)
const result = await run(compiler.bytecode, nativeFunctions)
const result = await run(compiler.bytecode, globalFunctions)
let value = VMResultToValue(result)
// Just treat regex as strings for comparison purposes