Compare commits
34 Commits
05cf93ab36
...
4f092fca3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f092fca3f | ||
| b50dafe79d | |||
| af7f389a04 | |||
| 67a98837ed | |||
| b42675e1d9 | |||
| 354df33894 | |||
| 66d21ce72b | |||
| d7bcc590fe | |||
| 787d2f2611 | |||
| bbc9316074 | |||
| 4a2e1f094a | |||
|
|
825487f2e0 | ||
|
|
0788f830bc | ||
| c032192d61 | |||
| c6c2646366 | |||
| 71fdafa72d | |||
| 318142dfbb | |||
| ffdd666685 | |||
| 0fc1f9f895 | |||
| cdcaf5c9d3 | |||
| 6c8c07e869 | |||
| 2fcd840493 | |||
| 28fab1235c | |||
| cbd3fe6315 | |||
| 6e432dd7a1 | |||
| 050acbfaeb | |||
| 34d1b8b998 | |||
| 219142140c | |||
| 972fd25fda | |||
| abd7d2e43b | |||
| 7cf7ac3703 | |||
| 299ad2c9a9 | |||
| e4100c7d89 | |||
| dba8430d9a |
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,3 +0,0 @@
|
|||
[submodule "packages/ReefVM"]
|
||||
path = packages/ReefVM
|
||||
url = git@54.219.130.253:defunkt/ReefVM.git
|
||||
93
bin/repl
93
bin/repl
|
|
@ -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()
|
||||
|
|
|
|||
45
bin/shrimp
45
bin/shrimp
|
|
@ -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) {
|
||||
|
|
|
|||
17
bun.lock
17
bun.lock
|
|
@ -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=="],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)*
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 h₂o`)
|
||||
})
|
||||
})
|
||||
|
||||
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`)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
141
src/prelude/index.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
4
src/prelude/tests/math.sh
Normal file
4
src/prelude/tests/math.sh
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
🥧 = 3.14159265359
|
||||
pi = 3.14
|
||||
add1 = do x: x + 1 end
|
||||
double = do x: x * 2 end
|
||||
256
src/prelude/tests/prelude.test.ts
Normal file
256
src/prelude/tests/prelude.test.ts
Normal 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("\\'")
|
||||
})
|
||||
})
|
||||
28
src/prelude/tests/use.test.ts
Normal file
28
src/prelude/tests/use.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user