Compare commits

...

49 Commits

Author SHA1 Message Date
1053a5ff52 Make dot-get work in the compiler AND with parens exprs 2025-10-29 13:19:47 -07:00
00eb1cf6f1 Merge pull request 'Prelude of builtin functions' (#7) from prelude into main
Reviewed-on: #7
Reviewed-by: probablycorey <probablycorey@gmail.com>
2025-10-29 20:15:36 +00:00
0de72a0d67 update reef 2025-10-29 13:14:59 -07:00
d939322f6e shh 2025-10-29 13:14:37 -07:00
92ce43b508 dict.set 2025-10-29 13:12:40 -07:00
c51030b3bd sure 2025-10-29 12:50:56 -07:00
e95c0d6728 use bun's equal check 2025-10-29 12:50:09 -07:00
c3453fdc5c how did that get in there... 2025-10-29 12:45:27 -07:00
1a3f1c6c43 tweak describe 2025-10-29 12:28:51 -07:00
a21ba54ad7 describe? 2025-10-29 12:21:11 -07:00
df3d483de5 update repl and shrimp 2025-10-29 12:21:02 -07:00
4fb58483f0 split up prelude modules 2025-10-29 12:13:56 -07:00
9e38fa7a44 globalFunctions => globals 2025-10-29 12:04:14 -07:00
3c06cac36c more prelude functions 2025-10-29 11:34:04 -07:00
51b64da106 update conditional tests 2025-10-29 11:21:24 -07:00
0dbba4d847 allow conditionals at statement-level 2025-10-29 11:21:16 -07:00
34305b473e switch back to using == 2025-10-29 11:18:47 -07:00
fd197a2dfc fix or/and chaining 2025-10-29 11:14:11 -07:00
ced190488a Add % operator 2025-10-29 10:58:48 -07:00
d843071bee prelude tests 2025-10-29 10:53:35 -07:00
40a648cd19 allow ? in identifier name 2025-10-29 10:38:57 -07:00
07ffc7df97 str.trim and friends, list.map and friends 2025-10-29 10:21:20 -07:00
3496b29072 tell the parser about builtin global functions 2025-10-29 10:18:19 -07:00
0eca3685f5 spruce up tests 2025-10-28 22:31:36 -07:00
Chris Wanstrath
dd2edb6dda prelude tests 2025-10-28 22:25:41 -07:00
b738e6cfd1 use -> load 2025-10-28 22:23:49 -07:00
bf1196bf96 use works more like fn, for now 2025-10-28 22:20:03 -07:00
f25ec024c2 further activate dotget 2025-10-28 22:18:46 -07:00
6d19896d1a not anymore, right 2025-10-28 21:53:22 -07:00
f08b16824a Merge branch 'list-and-dict-literals' into prelude 2025-10-28 21:52:55 -07:00
e1ba9c630d important note 2025-10-28 21:52:45 -07:00
b03610761b shh 2025-10-28 21:52:15 -07:00
b46154f753 no more, i think? 2025-10-28 21:38:56 -07:00
3a04970dca need you 2025-10-28 21:38:34 -07:00
2ff4615aab use module 2025-10-28 21:38:34 -07:00
7387c56a20 native -> global 2025-10-28 21:38:34 -07:00
d3e83e17b2 narrow type 2025-10-28 21:38:32 -07:00
9345c743ff no valueFunctions 2025-10-28 21:37:39 -07:00
ee4de6c59e update-reef command 2025-10-28 21:37:39 -07:00
35e6b63499 better echo 2025-10-28 21:37:39 -07:00
62036b1e4b start on a prelude of builtin functions 2025-10-28 21:37:39 -07:00
1aa1570135 add barus minimus docs 2025-10-28 21:36:02 -07:00
8112515278 [ = ] 2025-10-28 21:18:24 -07:00
982054eb54 [a=1 b=2 c=3] and [=] (empty dict) 2025-10-28 21:10:33 -07:00
34c1177636 more tests 2025-10-28 17:03:41 -07:00
339c09eb8c compile array literals 2025-10-28 16:47:33 -07:00
7da4c14962 parse arrays 2025-10-28 16:30:45 -07:00
Chris Wanstrath
7a4affd01e globals, not global functions 2025-10-28 13:08:51 -07:00
Chris Wanstrath
20e2dd3b31 update reef 2025-10-28 13:06:54 -07:00
31 changed files with 2029 additions and 268 deletions

View File

@ -207,6 +207,19 @@ Implementation files:
**Why this matters**: This enables shell-like file paths (`readme.txt`) while supporting dictionary/array access (`config.path`) without quotes, determined entirely at parse time based on lexical scope.
**Array and dict literals**: Square brackets `[]` create both arrays and dicts, distinguished by content:
- **Arrays**: Space/newline/semicolon-separated args that work like calling a function → `[1 2 3]` (call functions using parens eg `[1 (double 4) 200]`)
- **Dicts**: NamedArg syntax (key=value pairs) → `[a=1 b=2]`
- **Empty array**: `[]` (standard empty brackets)
- **Empty dict**: `[=]` (exactly this, no spaces)
Implementation details:
- Grammar rules (shrimp.grammar:194-201): Dict uses `NamedArg` nodes, Array uses `expression` nodes
- Parser distinguishes at parse time based on whether first element contains `=`
- Both support multiline, comments, and nesting
- Separators: spaces, newlines (`\n`), or semicolons (`;`) work interchangeably
- Test files: `src/parser/tests/literals.test.ts` and `src/compiler/tests/literals.test.ts`
**EOF handling**: The grammar uses `(statement | newlineOrSemicolon)+ eof?` to handle empty lines and end-of-file without infinite loops.
## Compiler Architecture

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, globals } 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: [] }, globals)
if (['/exit', 'exit', '/quit', 'quit'].includes(trimmed)) {
console.log(`\n${colors.yellow}Goodbye!${colors.reset}`)
@ -159,7 +147,7 @@ async function repl() {
codeHistory.push(trimmed)
try {
const compiler = new Compiler(trimmed)
const compiler = new Compiler(trimmed, Object.keys(globals))
vm.appendBytecode(compiler.bytecode)
@ -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: [] }, globals)
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, globals } 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 compiler = new Compiler(code, Object.keys(globals))
const vm = new VM(compiler.bytecode, globals)
await vm.run()
return vm.stack.length ? fromValue(vm.stack[vm.stack.length - 1]) : null
} catch (error: any) {

View File

@ -62,7 +62,7 @@
"hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="],
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#97b6722a113417398a1c47d583bfe07a906f87a0", { "peerDependencies": { "typescript": "^5" } }, "97b6722a113417398a1c47d583bfe07a906f87a0"],
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#030eb7487165b3ba502965a8b7fa09c4b5fdb0da", { "peerDependencies": { "typescript": "^5" } }, "030eb7487165b3ba502965a8b7fa09c4b5fdb0da"],
"style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="],

View File

@ -42,13 +42,13 @@ a-file = file.txt
3
# symbols can be assigned to functions. The body of the function comes after a colon `:`
add = fn x y: x + y
add = do x y: x + y
add 1 2
---
3
# Functions can have multiple lines, they are terminated with `end`
sub = fn x y:
sub = do x y:
x - y
end
@ -82,9 +82,25 @@ add 1 (sub 5 2)
4
# Arrays use square brackets with space-separated elements
numbers = [1 2 3]
shopping-list = [apples bananas carrots]
empty-array = []
# Dicts use square brackets with key=value pairs
config = [name=Shrimp version=1.0 debug=true]
empty-dict = [=]
# Nested structures work naturally
nested = [
users=[
[name=Alice age=30]
[name=Bob age=25]
]
settings=[debug=true timeout=5000]
]
# HOLD UP
- how do we handle arrays?
- how do we handle hashes?
- conditionals
- loops

View File

@ -3,21 +3,19 @@
"version": "0.1.0",
"private": true,
"type": "module",
"workspaces": [
"packages/*"
],
"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": "git+https://git.nose.space/defunkt/reefvm",
"@codemirror/view": "^6.38.3",
"@lezer/generator": "^1.8.0",
"bun-plugin-tailwind": "^0.0.15",
"codemirror": "^6.0.2",
"hono": "^4.9.8",
"reefvm": "git+https://git.nose.space/defunkt/reefvm",
"tailwindcss": "^4.1.11"
},
"devDependencies": {

View File

@ -1,6 +1,7 @@
import { CompilerError } from '#compiler/compilerError.ts'
import { parser } from '#parser/shrimp.ts'
import * as terms from '#parser/shrimp.terms'
import { setGlobals } from '#parser/tokenizer'
import type { SyntaxNode, Tree } from '@lezer/common'
import { assert, errorMessage } from '#utils/utils'
import { toBytecode, type Bytecode, type ProgramItem, bytecodeToString } from 'reefvm'
@ -53,8 +54,9 @@ export class Compiler {
bytecode: Bytecode
pipeCounter = 0
constructor(public input: string) {
constructor(public input: string, globals?: string[]) {
try {
if (globals) setGlobals(globals)
const cst = parser.parse(input)
const errors = checkTreeForErrors(cst)
@ -94,7 +96,6 @@ export class Compiler {
#compileNode(node: SyntaxNode, input: string): ProgramItem[] {
const value = input.slice(node.from, node.to)
if (DEBUG) console.log(`🫦 ${node.name}: ${value}`)
switch (node.type.id) {
@ -190,10 +191,15 @@ export class Compiler {
}
case terms.DotGet: {
const { objectName, propertyName } = getDotGetParts(node, input)
const { objectName, property } = getDotGetParts(node, input)
const instructions: ProgramItem[] = []
instructions.push(['TRY_LOAD', objectName])
instructions.push(['PUSH', propertyName])
if (property.type.id === terms.ParenExpr) {
instructions.push(...this.#compileNode(property, input))
} else {
const propertyValue = input.slice(property.from, property.to)
instructions.push(['PUSH', propertyValue])
}
instructions.push(['DOT_GET'])
return instructions
}
@ -218,6 +224,9 @@ export class Compiler {
case '/':
instructions.push(['DIV'])
break
case '%':
instructions.push(['MOD'])
break
default:
throw new CompilerError(`Unsupported binary operator: ${opValue}`, op.from, op.to)
}
@ -265,6 +274,10 @@ export class Compiler {
}
case terms.FunctionCallOrIdentifier: {
if (node.firstChild?.type.id === terms.DotGet) {
return this.#compileNode(node.firstChild, input)
}
return [['TRY_CALL', value]]
}
@ -358,7 +371,7 @@ export class Compiler {
const opValue = input.slice(op.from, op.to)
switch (opValue) {
case '=':
case '==':
instructions.push(...leftInstructions, ...rightInstructions, ['EQ'])
break
@ -468,6 +481,44 @@ export class Compiler {
return instructions
}
case terms.Array: {
const children = getAllChildren(node)
// We can easily parse [=] as an empty dict, but `[ = ]` is tougher.
// = can be a valid word, and is also valid inside words, so for now we cheat
// and check for arrays that look like `[ = ]` to interpret them as
// empty dicts
if (children.length === 1 && children[0]!.name === 'Word') {
const child = children[0]!
if (input.slice(child.from, child.to) === '=') {
return [['MAKE_DICT', 0]]
}
}
const instructions: ProgramItem[] = children.map((x) => this.#compileNode(x, input)).flat()
instructions.push(['MAKE_ARRAY', children.length])
return instructions
}
case terms.Dict: {
const children = getAllChildren(node)
const instructions: ProgramItem[] = []
children.forEach((node) => {
const keyNode = node.firstChild
const valueNode = node.firstChild!.nextSibling
// name= -> name
const key = input.slice(keyNode!.from, keyNode!.to).slice(0, -1)
instructions.push(['PUSH', key])
instructions.push(...this.#compileNode(valueNode!, input))
})
instructions.push(['MAKE_DICT', children.length])
return instructions
}
default:
throw new CompilerError(
`Compiler doesn't know how to handle a "${node.type.name}" node.`,

View File

@ -38,6 +38,12 @@ describe('compiler', () => {
expect('15 / 3').toEvaluateTo(5)
})
test('modulo', () => {
expect('44 % 2').toEvaluateTo(0)
expect('44 % 3').toEvaluateTo(2)
expect('3 % 4').toEvaluateTo(3)
})
test('assign number', () => {
expect('x = 5').toEvaluateTo(5)
})
@ -96,8 +102,7 @@ describe('compiler', () => {
end
abc
`)
.toEvaluateTo(true)
`).toEvaluateTo(true)
})
test('simple conditionals', () => {
@ -105,7 +110,7 @@ describe('compiler', () => {
expect(`(10 > 20)`).toEvaluateTo(false)
expect(`(4 <= 9)`).toEvaluateTo(true)
expect(`(15 >= 20)`).toEvaluateTo(false)
expect(`(7 = 7)`).toEvaluateTo(true)
expect(`(7 == 7)`).toEvaluateTo(true)
expect(`(5 != 5)`).toEvaluateTo(false)
expect(`('shave' and 'haircut')`).toEvaluateTo('haircut')
expect(`(false and witness)`).toEvaluateTo(false)
@ -238,3 +243,20 @@ describe('native functions', () => {
expect(`add 5 9`).toEvaluateTo(14, { add })
})
})
describe('dot get', () => {
const array = (...items: any) => items
const dict = (atNamed: any) => atNamed
test('access array element', () => {
expect(`arr = array 'a' 'b' 'c'; arr.1`).toEvaluateTo('b', { array })
})
test('access dict element', () => {
expect(`dict = dict a=1 b=2; dict.a`).toEvaluateTo(1, { dict })
})
test('use parens expr with dot-get', () => {
expect(`a = 1; arr = array 'a' 'b' 'c'; arr.(1 + a)`).toEvaluateTo('c', { array })
})
})

View File

@ -0,0 +1,157 @@
import { describe } from 'bun:test'
import { expect, test } from 'bun:test'
describe('array literals', () => {
test('work with numbers', () => {
expect('[1 2 3]').toEvaluateTo([1, 2, 3])
})
test('work with strings', () => {
expect("['one' 'two' 'three']").toEvaluateTo(['one', 'two', 'three'])
})
test('work with identifiers', () => {
expect('[one two three]').toEvaluateTo(['one', 'two', 'three'])
})
test('can be nested', () => {
expect('[one [two [three]]]').toEvaluateTo(['one', ['two', ['three']]])
})
test('can span multiple lines', () => {
expect(`[
1
2
3
]`).toEvaluateTo([1, 2, 3])
})
test('can span multiple w/o calling functions', () => {
expect(`[
one
two
three
]`).toEvaluateTo(['one', 'two', 'three'])
})
test('empty arrays', () => {
expect('[]').toEvaluateTo([])
})
test('mixed types', () => {
expect("[1 'two' three true null]").toEvaluateTo([1, 'two', 'three', true, null])
})
test('semicolons as separators', () => {
expect('[1; 2; 3]').toEvaluateTo([1, 2, 3])
})
test('expressions in arrays', () => {
expect('[(1 + 2) (3 * 4)]').toEvaluateTo([3, 12])
})
test('mixed separators - spaces and newlines', () => {
expect(`[1 2
3 4]`).toEvaluateTo([1, 2, 3, 4])
})
test('mixed separators - spaces and semicolons', () => {
expect('[1 2; 3 4]').toEvaluateTo([1, 2, 3, 4])
})
test('empty lines within arrays', () => {
expect(`[1
2]`).toEvaluateTo([1, 2])
})
test('comments within arrays', () => {
expect(`[1 # first
2 # second
]`).toEvaluateTo([1, 2])
})
test('complex nested multiline', () => {
expect(`[
[1 2]
[3 4]
[5 6]
]`).toEvaluateTo([
[1, 2],
[3, 4],
[5, 6],
])
})
test('boolean and null literals', () => {
expect('[true false null]').toEvaluateTo([true, false, null])
})
test('regex literals', () => {
expect('[//[0-9]+//]').toEvaluateTo([/[0-9]+/])
})
test('trailing newlines', () => {
expect(`[
1
2
]`).toEvaluateTo([1, 2])
})
})
describe('dict literals', () => {
test('work with numbers', () => {
expect('[a=1 b=2 c=3]').toEvaluateTo({ a: 1, b: 2, c: 3 })
})
test('work with strings', () => {
expect("[a='one' b='two' c='three']").toEvaluateTo({ a: 'one', b: 'two', c: 'three' })
})
test('work with identifiers', () => {
expect('[a=one b=two c=three]').toEvaluateTo({ a: 'one', b: 'two', c: 'three' })
})
test('can be nested', () => {
expect('[a=one b=[two [c=three]]]').toEvaluateTo({ a: 'one', b: ['two', { c: 'three' }] })
})
test('can span multiple lines', () => {
expect(`[
a=1
b=2
c=3
]`).toEvaluateTo({ a: 1, b: 2, c: 3 })
})
test('empty dict', () => {
expect('[=]').toEvaluateTo({})
expect('[ = ]').toEvaluateTo({})
})
test('mixed types', () => {
expect("[a=1 b='two' c=three d=true e=null]").toEvaluateTo({
a: 1,
b: 'two',
c: 'three',
d: true,
e: null,
})
})
test('semicolons as separators', () => {
expect('[a=1; b=2; c=3]').toEvaluateTo({ a: 1, b: 2, c: 3 })
})
test('expressions in dicts', () => {
expect('[a=(1 + 2) b=(3 * 4)]').toEvaluateTo({ a: 3, b: 12 })
})
test('empty lines within dicts', () => {
expect(`[a=1
b=2
c=3]`).toEvaluateTo({ a: 1, b: 2, c: 3 })
})
})

View File

@ -203,7 +203,7 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
const children = getAllChildren(node)
const [object, property] = children
if (children.length !== 2) {
if (!object || !property) {
throw new CompilerError(
`DotGet expected 2 identifier children, got ${children.length}`,
node.from,
@ -219,7 +219,7 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
)
}
if (property.type.id !== terms.Identifier && property.type.id !== terms.Number) {
if (![terms.Identifier, terms.Number, terms.ParenExpr].includes(property.type.id)) {
throw new CompilerError(
`DotGet property must be an Identifier or Number, got ${property.type.name}`,
property.from,
@ -228,7 +228,6 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
}
const objectName = input.slice(object.from, object.to)
const propertyName = input.slice(property.from, property.to)
return { objectName, propertyName }
return { objectName, property }
}

View File

@ -32,7 +32,6 @@ export const Editor = () => {
})
multilineModeSignal.connect((isMultiline) => {
console.log(`🌭 hey babe`, isMultiline)
view.dispatch({
effects: lineNumbersCompartment.reconfigure(isMultiline ? lineNumbers() : []),
})

View File

@ -8,6 +8,7 @@ const operators: Array<Operator> = [
{ str: '>=', tokenName: 'Gte' },
{ str: '<=', tokenName: 'Lte' },
{ str: '!=', tokenName: 'Neq' },
{ str: '==', tokenName: 'EqEq' },
// // Single-char operators
{ str: '*', tokenName: 'Star' },
@ -17,6 +18,7 @@ const operators: Array<Operator> = [
{ str: '-', tokenName: 'Minus' },
{ str: '>', tokenName: 'Gt' },
{ str: '<', tokenName: 'Lt' },
{ str: '%', tokenName: 'Modulo' },
]
export const operatorTokenizer = new ExternalTokenizer((input: InputStream) => {

View File

@ -6,7 +6,7 @@
@top Program { item* }
@external tokens operatorTokenizer from "./operatorTokenizer" { Star, Slash, Plus, Minus, And, Or, Eq, Neq, Lt, Lte, Gt, Gte }
@external tokens operatorTokenizer from "./operatorTokenizer" { Star, Slash, Plus, Minus, And, Or, Eq, EqEq, Neq, Lt, Lte, Gt, Gte, Modulo }
@tokens {
@precedence { Number Regex }
@ -33,6 +33,9 @@
@precedence {
pipe @left,
or @left,
and @left,
comparison @left,
multiplicative @left,
additive @left,
call
@ -52,6 +55,7 @@ consumeToTerminator {
FunctionDef |
Assign |
BinOp |
ConditionalOp |
expressionWithoutIdentifier
}
@ -129,14 +133,14 @@ SingleLineThenBlock {
}
ConditionalOp {
expression Eq expression |
expression Neq expression |
expression Lt expression |
expression Lte expression |
expression Gt expression |
expression Gte expression |
expression And (expression | ConditionalOp) |
expression Or (expression | ConditionalOp)
expression !comparison EqEq expression |
expression !comparison Neq expression |
expression !comparison Lt expression |
expression !comparison Lte expression |
expression !comparison Gt expression |
expression !comparison Gte expression |
(expression | ConditionalOp) !and And (expression | ConditionalOp) |
(expression | ConditionalOp) !or Or (expression | ConditionalOp)
}
Params {
@ -148,6 +152,7 @@ Assign {
}
BinOp {
expression !multiplicative Modulo expression |
(expression | BinOp) !multiplicative Star (expression | BinOp) |
(expression | BinOp) !multiplicative Slash (expression | BinOp) |
(expression | BinOp) !additive Plus (expression | BinOp) |
@ -169,7 +174,7 @@ expression {
@skip {} {
DotGet {
IdentifierBeforeDot dot (Number | Identifier)
IdentifierBeforeDot dot (Number | Identifier | ParenExpr)
}
String { "'" stringContent* "'" }
@ -191,6 +196,15 @@ EscapeSeq {
"\\" ("$" | "n" | "t" | "r" | "\\" | "'")
}
Dict {
"[=]" |
"[" newlineOrSemicolon* NamedArg (newlineOrSemicolon | NamedArg)* "]"
}
Array {
"[" newlineOrSemicolon* (expression (newlineOrSemicolon | expression)*)? "]"
}
// We need expressionWithoutIdentifier to avoid conflicts in consumeToTerminator.
// Without this, when parsing "my-var" at statement level, the parser can't decide:
// - ambiguousFunctionCall → FunctionCallOrIdentifier → Identifier
@ -200,7 +214,7 @@ EscapeSeq {
// to go through ambiguousFunctionCall (which is what we want semantically).
// Yes, it is annoying and I gave up trying to use GLR to fix it.
expressionWithoutIdentifier {
ParenExpr | Word | String | Number | Boolean | Regex | @specialize[@name=Null]<Identifier, "null">
ParenExpr | Word | String | Number | Boolean | Regex | Dict | Array | @specialize[@name=Null]<Identifier, "null">
}
block {

View File

@ -7,43 +7,47 @@ export const
And = 5,
Or = 6,
Eq = 7,
Neq = 8,
Lt = 9,
Lte = 10,
Gt = 11,
Gte = 12,
Identifier = 13,
AssignableIdentifier = 14,
Word = 15,
IdentifierBeforeDot = 16,
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,
PositionalArg = 38,
Underscore = 39,
NamedArg = 40,
NamedArgPrefix = 41,
IfExpr = 43,
SingleLineThenBlock = 45,
ThenBlock = 46,
ElseIfExpr = 47,
ElseExpr = 49,
Assign = 51
EqEq = 8,
Neq = 9,
Lt = 10,
Lte = 11,
Gt = 12,
Gte = 13,
Modulo = 14,
Identifier = 15,
AssignableIdentifier = 16,
Word = 17,
IdentifierBeforeDot = 18,
Do = 19,
Program = 20,
PipeExpr = 21,
FunctionCall = 22,
DotGet = 23,
Number = 24,
ParenExpr = 25,
FunctionCallOrIdentifier = 26,
BinOp = 27,
String = 28,
StringFragment = 29,
Interpolation = 30,
EscapeSeq = 31,
Boolean = 32,
Regex = 33,
Dict = 34,
NamedArg = 35,
NamedArgPrefix = 36,
FunctionDef = 37,
Params = 38,
colon = 39,
keyword = 54,
Underscore = 41,
Array = 42,
Null = 43,
ConditionalOp = 44,
PositionalArg = 45,
IfExpr = 47,
SingleLineThenBlock = 49,
ThenBlock = 50,
ElseIfExpr = 51,
ElseExpr = 53,
Assign = 55

View File

@ -4,24 +4,24 @@ import {operatorTokenizer} from "./operatorTokenizer"
import {tokenizer, specializeKeyword} from "./tokenizer"
import {trackScope} from "./scopeTracker"
import {highlighting} from "./highlight"
const spec_Identifier = {__proto__:null,null:64, end:74, if:88, elseif:96, else:100}
const spec_Identifier = {__proto__:null,end:80, null:86, if:96, elseif:104, else:108}
export const parser = LRParser.deserialize({
version: 14,
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,
states: "3UQYQbOOO#hQcO'#CvO$eOSO'#CxO$sQbO'#EVOOQ`'#DR'#DROOQa'#DO'#DOO%vQbO'#DWO&{QcO'#DzOOQa'#Dz'#DzO)PQcO'#DyO)xQRO'#CwO*]QcO'#DuO*tQcO'#DuO+VQbO'#CuO+}OpO'#CsOOQ`'#Dv'#DvO,SQbO'#DuO,bQbO'#E]OOQ`'#D]'#D]O-VQRO'#DeOOQ`'#Du'#DuO-[QQO'#DtOOQ`'#Dt'#DtOOQ`'#Df'#DfQYQbOOO-dQbO'#DPOOQa'#Dy'#DyOOQ`'#DZ'#DZOOQ`'#E['#E[OOQ`'#Dm'#DmO-nQbO,59^O.RQbO'#CzO.ZQWO'#C{OOOO'#D|'#D|OOOO'#Dg'#DgO.oOSO,59dOOQa,59d,59dOOQ`'#Di'#DiO.}QbO'#DSO/VQQO,5:qOOQ`'#Dh'#DhO/[QbO,59rO/cQQO,59jOOQa,59r,59rO/nQbO,59rO,bQbO,59cO,bQbO,59cO,bQbO,59cO,bQbO,59tO,bQbO,59tO,bQbO,59tO/xQRO,59aO0PQRO,59aO0bQRO,59aO0]QQO,59aO0mQQO,59aO0uObO,59_O1QQbO'#DnO1]QbO,59]O1nQRO,5:wO1uQRO,5:wO2QQbO,5:POOQ`,5:`,5:`OOQ`-E7d-E7dOOQ`,59k,59kOOQ`-E7k-E7kOOOO,59f,59fOOOO,59g,59gOOOO-E7e-E7eOOQa1G/O1G/OOOQ`-E7g-E7gO2[QbO1G0]OOQ`-E7f-E7fO2iQQO1G/UOOQa1G/^1G/^O2tQbO1G/^OOQO'#Dk'#DkO2iQQO1G/UOOQa1G/U1G/UOOQ`'#Dl'#DlO2tQbO1G/^OOQa1G.}1G.}O3gQcO1G.}O3qQcO1G.}O3{QcO1G.}OOQa1G/`1G/`O5_QcO1G/`O5fQcO1G/`O5mQcO1G/`OOQa1G.{1G.{OOQa1G.y1G.yO!ZQbO'#CvO%}QbO'#CrOOQ`,5:Y,5:YOOQ`-E7l-E7lO5tQbO1G0cOOQ`1G/k1G/kO6RQbO7+%wO6WQbO7+%xO6hQQO7+$pOOQa7+$p7+$pO6sQbO7+$xOOQa7+$x7+$xOOQO-E7i-E7iOOQ`-E7j-E7jOOQ`'#D_'#D_O6}QbO7+%}O7SQbO7+&OOOQ`<<Ic<<IcOOQ`'#Dj'#DjO7jQQO'#DjO7oQbO'#EXO8VQbO<<IdOOQa<<H[<<H[OOQa<<Hd<<HdOOQ`<<Ii<<IiOOQ`'#D`'#D`O8[QbO<<IjOOQ`,5:U,5:UOOQ`-E7h-E7hOOQ`AN?OAN?OO,bQbO'#DaOOQ`'#Do'#DoO8gQbOAN?UO8rQQO'#DcOOQ`AN?UAN?UO8wQbOAN?UO8|QRO,59{O9TQRO,59{OOQ`-E7m-E7mOOQ`G24pG24pO9`QbOG24pO9eQQO,59}O9jQQO1G/gOOQ`LD*[LD*[O6WQbO1G/iO7SQbO7+%ROOQ`7+%T7+%TOOQ`<<Hm<<Hm",
stateData: "9r~O!fOS!gOS~O_PO`cOaWOb^OcROhWOpWOqWO{WO!QaO!l]O!oQO!vTO!wUO!xfO~O_jOaWOb^OcROhWOpWOqWOtiOykO{WO!l]O!oQO!vTO!wUO!OjX!xjX#RjX!}jXxjX~OP!mXQ!mXR!mXS!mXT!mXU!mXW!mXX!mXY!mXZ!mX[!mX]!mX^!mX~P!ZOmqO!otO!qoO!rpO~O_uOwvP~O_jOaWOb^OhWOpWOqWOtiO{WO!l]O!oQO!vTO!wUO!xxO~O!|{O~P${O_jOaWOb^OcROhWOpWOqWOtiOykO{WO!l]O!oQO!vTO!wUO~OP!nXQ!nXR!nXS!nXT!nXU!nXW!nXX!nXY!nXZ!nX[!nX]!nX^!nX!x!nX#R!nX!}!nXx!nX~P%}OP!mXQ!mXR!mXS!mXT!mXU!mXW!mXX!mXY!mXZ!mX[!mX]!mX^!mX~O!x!iX#R!iXx!iX~P(UOT!ROU!SOW!QOX!QOY!QOZ!QO[!QO]!QO~OP!OOQ!OOR!POS!PO^}O~P)^OP!OOQ!OOR!POS!PO!x!iX#R!iXx!iX~OT!ROU!SO!x!iX#R!iXx!iX~O_POaWOb^OcROhWOpWOqWO{WO!l]O!oQO!vTO!wUO~O!k!YO~O!O!ZO!x!iX#R!iXx!iX~O_jOaWOb^OhWOpWOqWO{WO!l]O!oQO!vTO!wUO~OV!_O~O!x!`O#R!`O~OcROy!bO~P,bO!Ofa!xfa#Rfa!}faxfa~P%}O_!dO!l]O~O!o!eO!q!eO!r!eO!s!eO!t!eO!u!eO~OmqO!o!gO!qoO!rpO~O_uOwvX~Ow!iO~O!|!lO~P${OtiO!x!nO!|!pO~O!x!qO!|!lO~P,bO!}!{O~P(UOP!OOQ!OOR!POS!PO!}!{O~OT!ROU!SO!}!{O~O!O!ZO!}!{O~O_!|Oh!|O!l]O~O_!}Ob^O!l]O~O!O!ZO!xea#Rea!}eaxea~Ow#RO~P)^OT!ROU!SOw#RO~O`cO!QaO~P+VO`cO!QaO!x#UO~P+VOtiO!x!nO!|#WO~O!x!qO!|#YO~P,bO^}ORkiSki!xki#Rki!}kixki~OPkiQki~P3OOP!OOQ!OO~P3OOP!OOQ!OORkiSki!xki#Rki!}kixki~OW!QOX!QOY!QOZ!QO[!QO]!QOT|i!x|i#R|i!}|iw|ix|i~OU!SO~P4gOU!SO~P4yOU|i~P4gO`cO!QaO!x#_O~P+VOx#`O~O`cO!QaO!x#aOx!{P~P+VOtiO!x!nO!|#eO~O!x!qO!|#fO~P,bOx#gO~O`cO!QaO!x#aOx!{P!U!{P!W!{P~P+VO!x#jO~O`cO!QaO!x#aOx!{X!U!{X!W!{X~P+VOx#lO~Ox#qO!U#mO!W#pO~Ox#vO!U#mO!W#pO~Ow#xO~Ox#vO~Ow#yO~P)^OT!ROU!SOw#yO~Ox#zO~O!x#{O~O!x#|O~Ohq~",
goto: ".}#RPPPPPPPPPPPPPPPPPPPPP#S#c#qP$i#c%g%|P&o&oPP%|&sP'W'qPPP%|P't(aP(hP(t(w)QP)UP(h)[)b)h)n)t)}*X*c*l*sPPPP*y*}+cPP+u-SP-yPPPPPPPP-}-}.bPP.j.q.qddOh!_!i#R#U#_#c#{#|R!W]i_O]h!Z!_!i#R#U#_#c#{#|fPO]h!_!i#R#U#_#c#{#|xjPUVainy|}!O!P!Q!R!S!m!r!}#O#X#mR!}!ZfVO]h!_!i#R#U#_#c#{#|xWPUVainy|}!O!P!Q!R!S!m!r!}#O#X#mQ!doQ!|!YR#O!ZdZOh!_!i#R#U#_#c#{#|Q!U]Q!s!OR!v!P!aWOPUV]ahiny|}!O!P!Q!R!S!_!i!m!r!}#O#R#U#X#_#c#m#{#|TqQsYlPVn!}#OQzUQ!kyX!nz!k!o#VddOh!_!i#R#U#_#c#{#|YkPVn!}#OQ!W]R!biRwRd[Oh!_!i#R#U#_#c#{#|Q!V]Q!^aQ!w!SQ!y!RR#t#mZlPVn!}#OedOh!_!i#R#U#_#c#{#|R#^#RQ#i#_Q#}#{R$O#|T#n#i#oQ#r#iR#w#oQhOR!ahQsQR!fsQyUR!jyQvRR!hvW#c#U#_#{#|R#k#cQ!ozQ#V!kT#Z!o#VQ!r|Q#X!mT#[!r#XWnPV!}#OR!cnS![`!XR#Q![Q#o#iR#u#oTgOhSeOhQ#S!_Q#T!iQ#]#RZ#b#U#_#c#{#|d`Oh!_!i#R#U#_#c#{#|Q!X]R#P!ZfYO]h!_!i#R#U#_#c#{#|YkPVn!}#OQ|UQ!]aQ!biQ!myW!q|!m!r#XQ!s}Q!t!OQ!u!PQ!w!QQ!x!RQ!z!SR#s#mdXOh!_!i#R#U#_#c#{#|xjPUVainy|}!O!P!Q!R!S!m!r!}#O#X#mR!T]TrQssSOPV]hin!_!i!}#O#R#U#_#c#{#|Q#d#UV#h#_#{#|ZmPVn!}#OebOh!_!i#R#U#_#c#{#|",
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Dict NamedArg NamedArgPrefix FunctionDef Params colon keyword Underscore Array Null ConditionalOp PositionalArg operator IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
maxTerm: 95,
context: trackScope,
nodeProps: [
["closedBy", 36,"end"]
["closedBy", 39,"end"]
],
propSources: [highlighting],
skippedNodes: [0],
repeatNodeCount: 7,
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
repeatNodeCount: 10,
tokenData: "AO~R|OX#{XY$jYZ%TZp#{pq$jqs#{st%ntu'Vuw#{wx'[xy'ayz'zz{#{{|(e|}#{}!O(e!O!P#{!P!Q+X!Q![)S![!]3t!]!^%T!^!}#{!}#O4_#O#P6T#P#Q6Y#Q#R#{#R#S6s#S#T#{#T#Y7^#Y#Z8l#Z#b7^#b#c<z#c#f7^#f#g=q#g#h7^#h#i>h#i#o7^#o#p#{#p#q@`#q;'S#{;'S;=`$d<%l~#{~O#{~~@yS$QUmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUmS!fYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UmS!xQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZmS!gYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!gYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O!q~~'aO!o~U'hUmS!lQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUmS!}QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWmSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZYmShQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OWmSOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oWmShQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^WmSOt#{uw#{x!P#{!P!Q+v!Q#O#{#P;'S#{;'S;=`$d<%lO#{U+{^mSOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q#{!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wU-O^mSqQOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q0o!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wQ.PXqQOY-zZ!P-z!P!Q.l!Q!}-z!}#O/Z#O#P0Y#P;'S-z;'S;=`0i<%lO-zQ.oP!P!Q.rQ.wUqQ#Z#[.r#]#^.r#a#b.r#g#h.r#i#j.r#m#n.rQ/^VOY/ZZ#O/Z#O#P/s#P#Q-z#Q;'S/Z;'S;=`0S<%lO/ZQ/vSOY/ZZ;'S/Z;'S;=`0S<%lO/ZQ0VP;=`<%l/ZQ0]SOY-zZ;'S-z;'S;=`0i<%lO-zQ0lP;=`<%l-zU0tWmSOt#{uw#{x!P#{!P!Q1^!Q#O#{#P;'S#{;'S;=`$d<%lO#{U1ebmSqQOt#{uw#{x#O#{#P#Z#{#Z#[1^#[#]#{#]#^1^#^#a#{#a#b1^#b#g#{#g#h1^#h#i#{#i#j1^#j#m#{#m#n1^#n;'S#{;'S;=`$d<%lO#{U2r[mSOY2mYZ#{Zt2mtu/Zuw2mwx/Zx#O2m#O#P/s#P#Q,w#Q;'S2m;'S;=`3h<%lO2mU3kP;=`<%l2mU3qP;=`<%l,wU3{UmSwQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4fW!wQmSOt#{uw#{x!_#{!_!`5O!`#O#{#P;'S#{;'S;=`$d<%lO#{U5TVmSOt#{uw#{x#O#{#P#Q5j#Q;'S#{;'S;=`$d<%lO#{U5qU!vQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6YO!r~U6aU!|QmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6zUmSyQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7cYmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{U8YUtQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U8qZmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#U9d#U#o7^#o;'S#{;'S;=`$d<%lO#{U9i[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#`7^#`#a:_#a#o7^#o;'S#{;'S;=`$d<%lO#{U:d[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#g7^#g#h;Y#h#o7^#o;'S#{;'S;=`$d<%lO#{U;_[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#X7^#X#Y<T#Y#o7^#o;'S#{;'S;=`$d<%lO#{U<[YpQmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=RY!sWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=xY!uWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^>o[!tWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#f7^#f#g?e#g#o7^#o;'S#{;'S;=`$d<%lO#{U?j[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#i7^#i#j;Y#j#o7^#o;'S#{;'S;=`$d<%lO#{U@gU!OQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~AOO#R~",
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!k~~", 11)],
topRules: {"Program":[0,20]},
specialized: [{term: 15, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 15, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
tokenPrec: 1135
})

View File

@ -36,6 +36,19 @@ describe('Identifier', () => {
FunctionCallOrIdentifier
Identifier 𝜋`)
})
test('parses identifiers with queries', () => {
expect('even? 20').toMatchTree(`
FunctionCall
Identifier even?
PositionalArg
Number 20`)
expect('even?').toMatchTree(`
FunctionCallOrIdentifier
Identifier even?`)
})
})
describe('Unicode Symbol Support', () => {
@ -395,6 +408,15 @@ describe('BinOp', () => {
`)
})
test('modulo tests', () => {
expect('4 % 3').toMatchTree(`
BinOp
Number 4
Modulo %
Number 3
`)
})
test('mixed operations with precedence', () => {
expect('2 + 3 * 4 - 5 / 1').toMatchTree(`
BinOp
@ -571,3 +593,89 @@ describe('Comments', () => {
Identifier prop`)
})
})
describe('Conditional ops', () => {
test('or can be chained', () => {
expect(`
is-positive = do x:
if x == 3 or x == 4 or x == 5:
true
end
end
`).toMatchTree(`
Assign
AssignableIdentifier is-positive
Eq =
FunctionDef
Do do
Params
Identifier x
colon :
IfExpr
keyword if
ConditionalOp
ConditionalOp
ConditionalOp
Identifier x
EqEq ==
Number 3
Or or
ConditionalOp
Identifier x
EqEq ==
Number 4
Or or
ConditionalOp
Identifier x
EqEq ==
Number 5
colon :
ThenBlock
Boolean true
keyword end
keyword end
`)
})
test('and can be chained', () => {
expect(`
is-positive = do x:
if x == 3 and x == 4 and x == 5:
true
end
end
`).toMatchTree(`
Assign
AssignableIdentifier is-positive
Eq =
FunctionDef
Do do
Params
Identifier x
colon :
IfExpr
keyword if
ConditionalOp
ConditionalOp
ConditionalOp
Identifier x
EqEq ==
Number 3
And and
ConditionalOp
Identifier x
EqEq ==
Number 4
And and
ConditionalOp
Identifier x
EqEq ==
Number 5
colon :
ThenBlock
Boolean true
keyword end
keyword end
`)
})
})

View File

@ -4,12 +4,12 @@ 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' end`).toMatchTree(`
expect(`if y == 1: 'cool' end`).toMatchTree(`
IfExpr
keyword if
ConditionalOp
Identifier y
Eq =
EqEq ==
Number 1
colon :
SingleLineThenBlock

View File

@ -274,4 +274,28 @@ end`).toMatchTree(`
Identifier heya
`)
})
test('can use the result of a parens expression as the property of dot get', () => {
expect('obj = list 1 2 3; obj.(1 + 2)').toMatchTree(`
Assign
AssignableIdentifier obj
Eq =
FunctionCall
Identifier list
PositionalArg
Number 1
PositionalArg
Number 2
PositionalArg
Number 3
FunctionCallOrIdentifier
DotGet
IdentifierBeforeDot obj
ParenExpr
BinOp
Number 1
Plus +
Number 2
`)
})
})

View File

@ -0,0 +1,492 @@
import { expect, describe, test } from 'bun:test'
import '../shrimp.grammar' // Importing this so changes cause it to retest!
describe('array literals', () => {
test('work with numbers', () => {
expect('[1 2 3]').toMatchTree(`
Array
Number 1
Number 2
Number 3
`)
})
test('work with strings', () => {
expect("['one' 'two' 'three']").toMatchTree(`
Array
String
StringFragment one
String
StringFragment two
String
StringFragment three
`)
})
test('work with identifiers', () => {
expect('[one two three]').toMatchTree(`
Array
Identifier one
Identifier two
Identifier three
`)
})
test('can be nested', () => {
expect('[one [two [three]]]').toMatchTree(`
Array
Identifier one
Array
Identifier two
Array
Identifier three
`)
})
test('can span multiple lines', () => {
expect(`[
1
2
3
]`).toMatchTree(`
Array
Number 1
Number 2
Number 3
`)
})
test('can span multiple w/o calling functions', () => {
expect(`[
one
two
three
]`).toMatchTree(`
Array
Identifier one
Identifier two
Identifier three
`)
})
test('empty arrays', () => {
expect('[]').toMatchTree(`
Array []
`)
})
test('mixed types', () => {
expect("[1 'two' three true null]").toMatchTree(`
Array
Number 1
String
StringFragment two
Identifier three
Boolean true
Null null
`)
})
test('semicolons as separators', () => {
expect('[1; 2; 3]').toMatchTree(`
Array
Number 1
Number 2
Number 3
`)
})
test('expressions in arrays', () => {
expect('[(1 + 2) (3 * 4)]').toMatchTree(`
Array
ParenExpr
BinOp
Number 1
Plus +
Number 2
ParenExpr
BinOp
Number 3
Star *
Number 4
`)
})
test('mixed separators - spaces and newlines', () => {
expect(`[1 2
3 4]`).toMatchTree(`
Array
Number 1
Number 2
Number 3
Number 4
`)
})
test('mixed separators - spaces and semicolons', () => {
expect('[1 2; 3 4]').toMatchTree(`
Array
Number 1
Number 2
Number 3
Number 4
`)
})
test('empty lines within arrays', () => {
expect(`[1
2]`).toMatchTree(`
Array
Number 1
Number 2
`)
})
test('comments within arrays', () => {
expect(`[ # something...
1 # first
2 # second
]`).toMatchTree(`
Array
Number 1
Number 2
`)
})
test('complex nested multiline', () => {
expect(`[
[1 2]
[3 4]
[5 6]
]`).toMatchTree(`
Array
Array
Number 1
Number 2
Array
Number 3
Number 4
Array
Number 5
Number 6
`)
})
test('boolean and null literals', () => {
expect('[true false null]').toMatchTree(`
Array
Boolean true
Boolean false
Null null
`)
})
test('regex literals', () => {
expect('[//[0-9]+//]').toMatchTree(`
Array
Regex //[0-9]+//
`)
})
test('trailing newlines', () => {
expect(`[
1
2
]`).toMatchTree(`
Array
Number 1
Number 2
`)
})
})
describe('dict literals', () => {
test('work with numbers', () => {
expect('[a=1 b=2 c=3]').toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Number 1
NamedArg
NamedArgPrefix b=
Number 2
NamedArg
NamedArgPrefix c=
Number 3
`)
})
test('work with strings', () => {
expect("[a='one' b='two' c='three']").toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
String
StringFragment one
NamedArg
NamedArgPrefix b=
String
StringFragment two
NamedArg
NamedArgPrefix c=
String
StringFragment three
`)
})
test('work with identifiers', () => {
expect('[a=one b=two c=three]').toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Identifier one
NamedArg
NamedArgPrefix b=
Identifier two
NamedArg
NamedArgPrefix c=
Identifier three
`)
})
test('can be nested', () => {
expect('[a=one b=[two [c=three]]]').toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Identifier one
NamedArg
NamedArgPrefix b=
Array
Identifier two
Dict
NamedArg
NamedArgPrefix c=
Identifier three
`)
})
test('can span multiple lines', () => {
expect(`[
a=1
b=2
c=3
]`).toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Number 1
NamedArg
NamedArgPrefix b=
Number 2
NamedArg
NamedArgPrefix c=
Number 3
`)
})
test('empty dict', () => {
expect('[=]').toMatchTree(`
Dict [=]
`)
expect('[ = ]').toMatchTree(`
Array
Word =
`)
})
test('mixed types', () => {
expect("[a=1 b='two' c=three d=true e=null]").toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Number 1
NamedArg
NamedArgPrefix b=
String
StringFragment two
NamedArg
NamedArgPrefix c=
Identifier three
NamedArg
NamedArgPrefix d=
Boolean true
NamedArg
NamedArgPrefix e=
Null null
`)
})
test('semicolons as separators', () => {
expect('[a=1; b=2; c=3]').toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Number 1
NamedArg
NamedArgPrefix b=
Number 2
NamedArg
NamedArgPrefix c=
Number 3
`)
})
test('expressions in dicts', () => {
expect('[a=(1 + 2) b=(3 * 4)]').toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
ParenExpr
BinOp
Number 1
Plus +
Number 2
NamedArg
NamedArgPrefix b=
ParenExpr
BinOp
Number 3
Star *
Number 4
`)
})
test('mixed separators - spaces and newlines', () => {
expect(`[a=1 b=2
c=3]`).toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Number 1
NamedArg
NamedArgPrefix b=
Number 2
NamedArg
NamedArgPrefix c=
Number 3
`)
})
test('empty lines within dicts', () => {
expect(`[a=1
b=2
c=3]`).toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Number 1
NamedArg
NamedArgPrefix b=
Number 2
NamedArg
NamedArgPrefix c=
Number 3
`)
})
test('comments within dicts', () => {
expect(`[ # something...
a=1 # first
b=2 # second
c=3
]`).toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Number 1
NamedArg
NamedArgPrefix b=
Number 2
NamedArg
NamedArgPrefix c=
Number 3
`)
})
test('complex nested multiline', () => {
expect(`[
a=[a=1 b=2]
b=[b=3 c=4]
c=[c=5 d=6]
]`).toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Dict
NamedArg
NamedArgPrefix a=
Number 1
NamedArg
NamedArgPrefix b=
Number 2
NamedArg
NamedArgPrefix b=
Dict
NamedArg
NamedArgPrefix b=
Number 3
NamedArg
NamedArgPrefix c=
Number 4
NamedArg
NamedArgPrefix c=
Dict
NamedArg
NamedArgPrefix c=
Number 5
NamedArg
NamedArgPrefix d=
Number 6
`)
})
test('boolean and null literals', () => {
expect('[a=true b=false c=null]').toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Boolean true
NamedArg
NamedArgPrefix b=
Boolean false
NamedArg
NamedArgPrefix c=
Null null
`)
})
test('regex literals', () => {
expect('[pattern=//[0-9]+//]').toMatchTree(`
Dict
NamedArg
NamedArgPrefix pattern=
Regex //[0-9]+//
`)
})
test('trailing newlines', () => {
expect(`[
a=1
b=2
c=3
]`).toMatchTree(`
Dict
NamedArg
NamedArgPrefix a=
Number 1
NamedArg
NamedArgPrefix b=
Number 2
NamedArg
NamedArgPrefix c=
Number 3
`)
})
})

View File

@ -6,6 +6,13 @@ export function specializeKeyword(ident: string) {
return ident === 'do' ? Do : -1
}
// tell the dotGet searcher about builtin globals
export const globals: string[] = []
export const setGlobals = (newGlobals: string[]) => {
globals.length = 0
globals.push(...newGlobals)
}
// The only chars that can't be words are whitespace, apostrophes, closing parens, and EOF.
export const tokenizer = new ExternalTokenizer(
@ -112,7 +119,7 @@ const consumeWordToken = (
}
// Track identifier validity: must be lowercase, digit, dash, or emoji/unicode
if (!isLowercaseLetter(ch) && !isDigit(ch) && ch !== 45 /* - */ && !isEmojiOrUnicode(ch)) {
if (!isLowercaseLetter(ch) && !isDigit(ch) && ch !== 45 /* - */ && ch !== 63 /* ? */ && !isEmojiOrUnicode(ch)) {
if (!canBeWord) break
isValidIdentifier = false
}
@ -152,7 +159,7 @@ const checkForDotGet = (input: InputStream, stack: Stack, pos: number): number |
// If identifier is in scope, this is property access (e.g., obj.prop)
// If not in scope, it should be consumed as a Word (e.g., file.txt)
return context?.scope.has(identifierText) ? IdentifierBeforeDot : null
return context?.scope.has(identifierText) || globals.includes(identifierText) ? IdentifierBeforeDot : null
}
// Decide between AssignableIdentifier and Identifier using grammar state + peek-ahead
@ -195,7 +202,13 @@ const isWhiteSpace = (ch: number): boolean => {
}
const isWordChar = (ch: number): boolean => {
return !isWhiteSpace(ch) && ch !== 10 /* \n */ && ch !== 41 /* ) */ && ch !== -1 /* EOF */
return (
!isWhiteSpace(ch) &&
ch !== 10 /* \n */ &&
ch !== 41 /* ) */ &&
ch !== 93 /* ] */ &&
ch !== -1 /* EOF */
)
}
const isLowercaseLetter = (ch: number): boolean => {

35
src/prelude/dict.ts Normal file
View File

@ -0,0 +1,35 @@
import { type Value, toString, toValue } from 'reefvm'
export const dict = {
keys: (dict: Record<string, any>) => Object.keys(dict),
values: (dict: Record<string, any>) => Object.values(dict),
entries: (dict: Record<string, any>) => Object.entries(dict).map(([k, v]) => ({ key: k, value: v })),
'has?': (dict: Record<string, any>, key: string) => key in dict,
get: (dict: Record<string, any>, key: string, defaultValue: any = null) => dict[key] ?? defaultValue,
set: (dict: Value, key: Value, value: Value) => {
const map = dict.value as Map<string, Value>
map.set(toString(key), value)
return dict
},
merge: (...dicts: Record<string, any>[]) => Object.assign({}, ...dicts),
'empty?': (dict: Record<string, any>) => Object.keys(dict).length === 0,
map: async (dict: Record<string, any>, cb: Function) => {
const result: Record<string, any> = {}
for (const [key, value] of Object.entries(dict)) {
result[key] = await cb(value, key)
}
return result
},
filter: async (dict: Record<string, any>, cb: Function) => {
const result: Record<string, any> = {}
for (const [key, value] of Object.entries(dict)) {
if (await cb(value, key)) result[key] = value
}
return result
},
'from-entries': (entries: [string, any][]) => Object.fromEntries(entries),
}
// raw functions deal directly in Value types, meaning we can modify collection
// careful - the MUST return a Value!
; (dict.set as any).raw = true

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

@ -0,0 +1,149 @@
// The prelude creates all the builtin Shrimp functions.
import {
type Value, toValue,
extractParamInfo, isWrapped, getOriginalFunction,
} from 'reefvm'
import { dict } from './dict'
import { load } from './load'
import { list } from './list'
import { math } from './math'
import { str } from './str'
export const globals = {
dict,
load,
list,
math,
str,
// 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)),
describe: (v: any) => {
const val = toValue(v)
return `#<${val.type}: ${formatValue(val)}>`
},
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
}
},
// type predicates
'string?': (v: any) => toValue(v).type === 'string',
'number?': (v: any) => toValue(v).type === 'number',
'boolean?': (v: any) => toValue(v).type === 'boolean',
'array?': (v: any) => toValue(v).type === 'array',
'dict?': (v: any) => toValue(v).type === 'dict',
'function?': (v: any) => {
const t = toValue(v).type
return t === 'function' || t === 'native'
},
'null?': (v: any) => toValue(v).type === 'null',
'some?': (v: any) => toValue(v).type !== 'null',
// boolean/logic
not: (v: any) => !v,
// utilities
inc: (n: number) => n + 1,
dec: (n: number) => n - 1,
identity: (v: any) => v,
// collections
at: (collection: any, index: number | string) => collection[index],
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
},
'empty?': (v: any) => {
const value = toValue(v)
switch (value.type) {
case 'string': case 'array':
return value.value.length === 0
case 'dict':
return value.value.size === 0
default:
return false
}
},
// enumerables
each: async (list: any[], cb: Function) => {
for (const value of list) await cb(value)
return list
},
}
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 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 `${colors.blue}[${colors.reset}${items}${colors.blue}]${colors.reset}`
}
case 'dict': {
const entries = Array.from(value.value.entries())
.map(([k, v]) => `${k}${colors.blue}=${colors.reset}${formatValue(v, true)}`)
.join(' ')
if (entries.length === 0)
return `${colors.blue}[=]${colors.reset}`
return `${colors.blue}[${colors.reset}${entries}${colors.blue}]${colors.reset}`
}
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)
}
}

89
src/prelude/list.ts Normal file
View File

@ -0,0 +1,89 @@
export const list = {
slice: (list: any[], start: number, end?: number) => list.slice(start, end),
map: async (list: any[], cb: Function) => {
let acc: any[] = []
for (const value of list) acc.push(await cb(value))
return acc
},
filter: async (list: any[], cb: Function) => {
let acc: any[] = []
for (const value of list) {
if (await cb(value)) acc.push(value)
}
return acc
},
reduce: async (list: any[], cb: Function, initial: any) => {
let acc = initial
for (const value of list) acc = await cb(acc, value)
return acc
},
find: async (list: any[], cb: Function) => {
for (const value of list) {
if (await cb(value)) return value
}
return null
},
// predicates
'empty?': (list: any[]) => list.length === 0,
'contains?': (list: any[], item: any) => list.includes(item),
'any?': async (list: any[], cb: Function) => {
for (const value of list) {
if (await cb(value)) return true
}
return false
},
'all?': async (list: any[], cb: Function) => {
for (const value of list) {
if (!await cb(value)) return false
}
return true
},
// sequence operations
reverse: (list: any[]) => list.slice().reverse(),
sort: (list: any[], cb?: (a: any, b: any) => number) => list.slice().sort(cb),
concat: (...lists: any[][]) => lists.flat(1),
flatten: (list: any[], depth: number = 1) => list.flat(depth),
unique: (list: any[]) => Array.from(new Set(list)),
zip: (list1: any[], list2: any[]) => list1.map((item, i) => [item, list2[i]]),
// access
first: (list: any[]) => list[0] ?? null,
last: (list: any[]) => list[list.length - 1] ?? null,
rest: (list: any[]) => list.slice(1),
take: (list: any[], n: number) => list.slice(0, n),
drop: (list: any[], n: number) => list.slice(n),
append: (list: any[], item: any) => [...list, item],
prepend: (list: any[], item: any) => [item, ...list],
'index-of': (list: any[], item: any) => list.indexOf(item),
// utilities
sum: (list: any[]) => list.reduce((acc, x) => acc + x, 0),
count: async (list: any[], cb: Function) => {
let count = 0
for (const value of list) {
if (await cb(value)) count++
}
return count
},
partition: async (list: any[], cb: Function) => {
const truthy: any[] = []
const falsy: any[] = []
for (const value of list) {
if (await cb(value)) truthy.push(value)
else falsy.push(value)
}
return [truthy, falsy]
},
compact: (list: any[]) => list.filter(x => x != null),
'group-by': async (list: any[], cb: Function) => {
const groups: Record<string, any[]> = {}
for (const value of list) {
const key = String(await cb(value))
if (!groups[key]) groups[key] = []
groups[key].push(value)
}
return groups
},
}

29
src/prelude/load.ts Normal file
View File

@ -0,0 +1,29 @@
import { resolve } from 'path'
import { readFileSync } from 'fs'
import { Compiler } from '#compiler/compiler'
import { type Value, VM, Scope } from 'reefvm'
export const load = async function (this: VM, path: string): Promise<Record<string, Value>> {
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: Record<string, Value> = {}
for (const [name, value] of this.scope.locals.entries())
module[name] = value
this.scope = scope
this.pc = pc
this.stopped = false
return module
}

21
src/prelude/math.ts Normal file
View File

@ -0,0 +1,21 @@
export const math = {
abs: (n: number) => Math.abs(n),
floor: (n: number) => Math.floor(n),
ceil: (n: number) => Math.ceil(n),
round: (n: number) => Math.round(n),
min: (...nums: number[]) => Math.min(...nums),
max: (...nums: number[]) => Math.max(...nums),
pow: (base: number, exp: number) => Math.pow(base, exp),
sqrt: (n: number) => Math.sqrt(n),
random: () => Math.random(),
clamp: (n: number, min: number, max: number) => Math.min(Math.max(n, min), max),
sign: (n: number) => Math.sign(n),
trunc: (n: number) => Math.trunc(n),
// predicates
'even?': (n: number) => n % 2 === 0,
'odd?': (n: number) => n % 2 !== 0,
'positive?': (n: number) => n > 0,
'negative?': (n: number) => n < 0,
'zero?': (n: number) => n === 0,
}

33
src/prelude/str.ts Normal file
View File

@ -0,0 +1,33 @@
// strings
export const str = {
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(),
// predicates
'starts-with?': (str: string, prefix: string) => str.startsWith(prefix),
'ends-with?': (str: string, suffix: string) => str.endsWith(suffix),
'contains?': (str: string, substr: string) => str.includes(substr),
'empty?': (str: string) => str.length === 0,
// inspection
'index-of': (str: string, search: string) => str.indexOf(search),
'last-index-of': (str: string, search: string) => str.lastIndexOf(search),
// transformations
replace: (str: string, search: string, replacement: string) => str.replace(search, replacement),
'replace-all': (str: string, search: string, replacement: string) => str.replaceAll(search, replacement),
slice: (str: string, start: number, end?: number | null) => str.slice(start, end ?? undefined),
substring: (str: string, start: number, end?: number | null) => str.substring(start, end ?? undefined),
repeat: (str: string, count: number) => str.repeat(count),
'pad-start': (str: string, length: number, pad: string = ' ') => str.padStart(length, pad),
'pad-end': (str: string, length: number, pad: string = ' ') => str.padEnd(length, pad),
lines: (str: string) => str.split('\n'),
chars: (str: string) => str.split(''),
// regex
match: (str: string, regex: RegExp) => str.match(regex),
'test?': (str: string, regex: RegExp) => regex.test(str),
}

42
src/prelude/tests/load.ts Normal file
View File

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

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,598 @@
import { expect, describe, test } from 'bun:test'
import { globals } from '#prelude'
describe('string operations', () => {
test('to-upper converts to uppercase', async () => {
await expect(`str.to-upper 'hello'`).toEvaluateTo('HELLO', globals)
await expect(`str.to-upper 'Hello World!'`).toEvaluateTo('HELLO WORLD!', globals)
})
test('to-lower converts to lowercase', async () => {
await expect(`str.to-lower 'HELLO'`).toEvaluateTo('hello', globals)
await expect(`str.to-lower 'Hello World!'`).toEvaluateTo('hello world!', globals)
})
test('trim removes whitespace', async () => {
await expect(`str.trim ' hello '`).toEvaluateTo('hello', globals)
await expect(`str.trim '\\n\\thello\\t\\n'`).toEvaluateTo('hello', globals)
})
test('split divides string by separator', async () => {
await expect(`str.split 'a,b,c' ','`).toEvaluateTo(['a', 'b', 'c'], globals)
await expect(`str.split 'hello' ''`).toEvaluateTo(['h', 'e', 'l', 'l', 'o'], globals)
})
test('split with comma separator', async () => {
await expect(`str.split 'a,b,c' ','`).toEvaluateTo(['a', 'b', 'c'], globals)
})
test('join combines array elements', async () => {
await expect(`str.join ['a' 'b' 'c'] '-'`).toEvaluateTo('a-b-c', globals)
await expect(`str.join ['hello' 'world'] ' '`).toEvaluateTo('hello world', globals)
})
test('join with comma separator', async () => {
await expect(`str.join ['a' 'b' 'c'] ','`).toEvaluateTo('a,b,c', globals)
})
test('starts-with? checks string prefix', async () => {
await expect(`str.starts-with? 'hello' 'hel'`).toEvaluateTo(true, globals)
await expect(`str.starts-with? 'hello' 'bye'`).toEvaluateTo(false, globals)
})
test('ends-with? checks string suffix', async () => {
await expect(`str.ends-with? 'hello' 'lo'`).toEvaluateTo(true, globals)
await expect(`str.ends-with? 'hello' 'he'`).toEvaluateTo(false, globals)
})
test('contains? checks for substring', async () => {
await expect(`str.contains? 'hello world' 'o w'`).toEvaluateTo(true, globals)
await expect(`str.contains? 'hello' 'bye'`).toEvaluateTo(false, globals)
})
test('empty? checks if string is empty', async () => {
await expect(`str.empty? ''`).toEvaluateTo(true, globals)
await expect(`str.empty? 'hello'`).toEvaluateTo(false, globals)
})
test('replace replaces first occurrence', async () => {
await expect(`str.replace 'hello hello' 'hello' 'hi'`).toEvaluateTo('hi hello', globals)
})
test('replace-all replaces all occurrences', async () => {
await expect(`str.replace-all 'hello hello' 'hello' 'hi'`).toEvaluateTo('hi hi', globals)
})
test('slice extracts substring', async () => {
await expect(`str.slice 'hello' 1 3`).toEvaluateTo('el', globals)
await expect(`str.slice 'hello' 2 null`).toEvaluateTo('llo', globals)
})
test('repeat repeats string', async () => {
await expect(`str.repeat 'ha' 3`).toEvaluateTo('hahaha', globals)
})
test('pad-start pads beginning', async () => {
await expect(`str.pad-start '5' 3 '0'`).toEvaluateTo('005', globals)
})
test('pad-end pads end', async () => {
await expect(`str.pad-end '5' 3 '0'`).toEvaluateTo('500', globals)
})
test('lines splits by newlines', async () => {
await expect(`str.lines 'a\\nb\\nc'`).toEvaluateTo(['a', 'b', 'c'], globals)
})
test('chars splits into characters', async () => {
await expect(`str.chars 'abc'`).toEvaluateTo(['a', 'b', 'c'], globals)
})
test('index-of finds substring position', async () => {
await expect(`str.index-of 'hello world' 'world'`).toEvaluateTo(6, globals)
await expect(`str.index-of 'hello' 'bye'`).toEvaluateTo(-1, globals)
})
test('last-index-of finds last occurrence', async () => {
await expect(`str.last-index-of 'hello hello' 'hello'`).toEvaluateTo(6, globals)
})
})
describe('type predicates', () => {
test('string? checks for string type', async () => {
await expect(`string? 'hello'`).toEvaluateTo(true, globals)
await expect(`string? 42`).toEvaluateTo(false, globals)
})
test('number? checks for number type', async () => {
await expect(`number? 42`).toEvaluateTo(true, globals)
await expect(`number? 'hello'`).toEvaluateTo(false, globals)
})
test('boolean? checks for boolean type', async () => {
await expect(`boolean? true`).toEvaluateTo(true, globals)
await expect(`boolean? 42`).toEvaluateTo(false, globals)
})
test('array? checks for array type', async () => {
await expect(`array? [1 2 3]`).toEvaluateTo(true, globals)
await expect(`array? 42`).toEvaluateTo(false, globals)
})
test('dict? checks for dict type', async () => {
await expect(`dict? [a=1]`).toEvaluateTo(true, globals)
await expect(`dict? []`).toEvaluateTo(false, globals)
})
test('null? checks for null type', async () => {
await expect(`null? null`).toEvaluateTo(true, globals)
await expect(`null? 42`).toEvaluateTo(false, globals)
})
test('some? checks for non-null', async () => {
await expect(`some? 42`).toEvaluateTo(true, globals)
await expect(`some? null`).toEvaluateTo(false, globals)
})
})
describe('boolean logic', () => {
test('not negates value', async () => {
await expect(`not true`).toEvaluateTo(false, globals)
await expect(`not false`).toEvaluateTo(true, globals)
await expect(`not 42`).toEvaluateTo(false, globals)
await expect(`not null`).toEvaluateTo(true, globals)
})
})
describe('utilities', () => {
test('inc increments by 1', async () => {
await expect(`inc 5`).toEvaluateTo(6, globals)
await expect(`inc -1`).toEvaluateTo(0, globals)
})
test('dec decrements by 1', async () => {
await expect(`dec 5`).toEvaluateTo(4, globals)
await expect(`dec 0`).toEvaluateTo(-1, globals)
})
test('identity returns value as-is', async () => {
await expect(`identity 42`).toEvaluateTo(42, globals)
await expect(`identity 'hello'`).toEvaluateTo('hello', globals)
})
})
describe('introspection', () => {
test('type returns proper types', async () => {
await expect(`type 'hello'`).toEvaluateTo('string', globals)
await expect(`type 42`).toEvaluateTo('number', globals)
await expect(`type true`).toEvaluateTo('boolean', globals)
await expect(`type false`).toEvaluateTo('boolean', globals)
await expect(`type null`).toEvaluateTo('null', globals)
await expect(`type [1 2 3]`).toEvaluateTo('array', globals)
await expect(`type [a=1 b=2]`).toEvaluateTo('dict', globals)
})
test('length', async () => {
await expect(`length 'hello'`).toEvaluateTo(5, globals)
await expect(`length [1 2 3]`).toEvaluateTo(3, globals)
await expect(`length [a=1 b=2]`).toEvaluateTo(2, globals)
await expect(`length 42`).toEvaluateTo(0, globals)
await expect(`length true`).toEvaluateTo(0, globals)
await expect(`length null`).toEvaluateTo(0, globals)
})
test('inspect formats values', async () => {
// Just test that inspect returns something for now
// (we'd need more complex assertion to check the actual format)
await expect(`type (inspect 'hello')`).toEvaluateTo('string', globals)
})
test('describe describes values', async () => {
// Just test that inspect returns something for now
// (we'd need more complex assertion to check the actual format)
await expect(`describe 'hello'`).toEvaluateTo("#<string: \u001b[32m'hello\u001b[32m'\u001b[0m>", globals)
})
})
describe('collections', () => {
test('literal array creates array from arguments', async () => {
await expect(`[ 1 2 3 ]`).toEvaluateTo([1, 2, 3], globals)
await expect(`['a' 'b']`).toEvaluateTo(['a', 'b'], globals)
await expect(`[]`).toEvaluateTo([], globals)
})
test('literal dict creates object from named arguments', async () => {
await expect(`[ a=1 b=2 ]`).toEvaluateTo({ a: 1, b: 2 }, globals)
await expect(`[=]`).toEvaluateTo({}, globals)
})
test('at retrieves element at index', async () => {
await expect(`at [10 20 30] 0`).toEvaluateTo(10, globals)
await expect(`at [10 20 30] 2`).toEvaluateTo(30, globals)
})
test('at retrieves property from object', async () => {
await expect(`at [name='test'] 'name'`).toEvaluateTo('test', globals)
})
test('slice extracts array subset', async () => {
await expect(`list.slice [1 2 3 4 5] 1 3`).toEvaluateTo([2, 3], globals)
await expect(`list.slice [1 2 3 4 5] 2 5`).toEvaluateTo([3, 4, 5], globals)
})
test('range creates number sequence', async () => {
await expect(`range 0 5`).toEvaluateTo([0, 1, 2, 3, 4, 5], globals)
await expect(`range 3 6`).toEvaluateTo([3, 4, 5, 6], globals)
})
test('range with single argument starts from 0', async () => {
await expect(`range 3 null`).toEvaluateTo([0, 1, 2, 3], globals)
await expect(`range 0 null`).toEvaluateTo([0], globals)
})
test('empty? checks if list, dict, string is empty', async () => {
await expect(`empty? []`).toEvaluateTo(true, globals)
await expect(`empty? [1]`).toEvaluateTo(false, globals)
await expect(`empty? [=]`).toEvaluateTo(true, globals)
await expect(`empty? [a=true]`).toEvaluateTo(false, globals)
await expect(`empty? ''`).toEvaluateTo(true, globals)
await expect(`empty? 'cat'`).toEvaluateTo(false, globals)
await expect(`empty? meow`).toEvaluateTo(false, globals)
})
test('list.filter keeps matching elements', async () => {
await expect(`
is-positive = do x:
x == 3 or x == 4 or x == 5
end
list.filter [1 2 3 4 5] is-positive
`).toEvaluateTo([3, 4, 5], globals)
})
test('list.reduce accumulates values', async () => {
await expect(`
add = do acc x:
acc + x
end
list.reduce [1 2 3 4] add 0
`).toEvaluateTo(10, globals)
})
test('list.find returns first match', async () => {
await expect(`
is-four = do x:
x == 4
end
list.find [1 2 4 5] is-four
`).toEvaluateTo(4, globals)
})
test('list.find returns null if no match', async () => {
await expect(`
is-ten = do x: x == 10 end
list.find [1 2 3] is-ten
`).toEvaluateTo(null, globals)
})
test('list.empty? checks if list is empty', async () => {
await expect(`list.empty? []`).toEvaluateTo(true, globals)
await expect(`list.empty? [1]`).toEvaluateTo(false, globals)
})
test('list.contains? checks for element', async () => {
await expect(`list.contains? [1 2 3] 2`).toEvaluateTo(true, globals)
await expect(`list.contains? [1 2 3] 5`).toEvaluateTo(false, globals)
})
test('list.reverse reverses array', async () => {
await expect(`list.reverse [1 2 3]`).toEvaluateTo([3, 2, 1], globals)
})
test('list.concat combines arrays', async () => {
await expect(`list.concat [1 2] [3 4]`).toEvaluateTo([1, 2, 3, 4], globals)
})
test('list.flatten flattens nested arrays', async () => {
await expect(`list.flatten [[1 2] [3 4]] 1`).toEvaluateTo([1, 2, 3, 4], globals)
})
test('list.unique removes duplicates', async () => {
await expect(`list.unique [1 2 2 3 1]`).toEvaluateTo([1, 2, 3], globals)
})
test('list.zip combines two arrays', async () => {
await expect(`list.zip [1 2] [3 4]`).toEvaluateTo([[1, 3], [2, 4]], globals)
})
test('list.first returns first element', async () => {
await expect(`list.first [1 2 3]`).toEvaluateTo(1, globals)
await expect(`list.first []`).toEvaluateTo(null, globals)
})
test('list.last returns last element', async () => {
await expect(`list.last [1 2 3]`).toEvaluateTo(3, globals)
await expect(`list.last []`).toEvaluateTo(null, globals)
})
test('list.rest returns all but first', async () => {
await expect(`list.rest [1 2 3]`).toEvaluateTo([2, 3], globals)
})
test('list.take returns first n elements', async () => {
await expect(`list.take [1 2 3 4 5] 3`).toEvaluateTo([1, 2, 3], globals)
})
test('list.drop skips first n elements', async () => {
await expect(`list.drop [1 2 3 4 5] 2`).toEvaluateTo([3, 4, 5], globals)
})
test('list.append adds to end', async () => {
await expect(`list.append [1 2] 3`).toEvaluateTo([1, 2, 3], globals)
})
test('list.prepend adds to start', async () => {
await expect(`list.prepend [2 3] 1`).toEvaluateTo([1, 2, 3], globals)
})
test('list.index-of finds element index', async () => {
await expect(`list.index-of [1 2 3] 2`).toEvaluateTo(1, globals)
await expect(`list.index-of [1 2 3] 5`).toEvaluateTo(-1, globals)
})
test('list.any? checks if any element matches', async () => {
await expect(`
gt-three = do x: x > 3 end
list.any? [1 2 4 5] gt-three
`).toEvaluateTo(true, globals)
await expect(`
gt-ten = do x: x > 10 end
list.any? [1 2 3] gt-ten
`).toEvaluateTo(false, globals)
})
test('list.all? checks if all elements match', async () => {
await expect(`
positive = do x: x > 0 end
list.all? [1 2 3] positive
`).toEvaluateTo(true, globals)
await expect(`
positive = do x: x > 0 end
list.all? [1 -2 3] positive
`).toEvaluateTo(false, globals)
})
test('list.sum adds all numbers', async () => {
await expect(`list.sum [1 2 3 4]`).toEvaluateTo(10, globals)
await expect(`list.sum []`).toEvaluateTo(0, globals)
})
test('list.count counts matching elements', async () => {
await expect(`
gt-two = do x: x > 2 end
list.count [1 2 3 4 5] gt-two
`).toEvaluateTo(3, globals)
})
test('list.partition splits array by predicate', async () => {
await expect(`
gt-two = do x: x > 2 end
list.partition [1 2 3 4 5] gt-two
`).toEvaluateTo([[3, 4, 5], [1, 2]], globals)
})
test('list.compact removes null values', async () => {
await expect(`list.compact [1 null 2 null 3]`).toEvaluateTo([1, 2, 3], globals)
})
test('list.group-by groups by key function', async () => {
await expect(`
get-type = do x:
if (string? x):
'str'
else:
'num'
end
end
list.group-by ['a' 1 'b' 2] get-type
`).toEvaluateTo({ str: ['a', 'b'], num: [1, 2] }, globals)
})
})
describe('enumerables', () => {
test('map transforms array elements', async () => {
await expect(`
double = do x: x * 2 end
list.map [1 2 3] double
`).toEvaluateTo([2, 4, 6], globals)
})
test('map handles empty array', async () => {
await expect(`
double = do x: x * 2 end
list.map [] double
`).toEvaluateTo([], globals)
})
test('each iterates over array', async () => {
// Note: each doesn't return the results, it returns null
// We can test it runs by checking the return value
await expect(`
double = do x: x * 2 end
each [1 2 3] double
`).toEvaluateTo([1, 2, 3], globals)
})
test('each handles empty array', async () => {
await expect(`
fn = do x: x end
each [] fn
`).toEvaluateTo([], globals)
})
})
describe('dict operations', () => {
test('dict.keys returns all keys', async () => {
const result = await (async () => {
const { Compiler } = await import('#compiler/compiler')
const { run, fromValue } = await import('reefvm')
const { setGlobals } = await import('#parser/tokenizer')
setGlobals(Object.keys(globals))
const c = new Compiler('dict.keys [a=1 b=2 c=3]')
const r = await run(c.bytecode, globals)
return fromValue(r)
})()
// Check that all expected keys are present (order may vary)
expect(result.sort()).toEqual(['a', 'b', 'c'])
})
test('dict.values returns all values', async () => {
const result = await (async () => {
const { Compiler } = await import('#compiler/compiler')
const { run, fromValue } = await import('reefvm')
const { setGlobals } = await import('#parser/tokenizer')
setGlobals(Object.keys(globals))
const c = new Compiler('dict.values [a=1 b=2]')
const r = await run(c.bytecode, globals)
return fromValue(r)
})()
// Check that all expected values are present (order may vary)
expect(result.sort()).toEqual([1, 2])
})
test('dict.has? checks for key', async () => {
await expect(`dict.has? [a=1 b=2] 'a'`).toEvaluateTo(true, globals)
await expect(`dict.has? [a=1 b=2] 'c'`).toEvaluateTo(false, globals)
})
test('dict.get retrieves value with default', async () => {
await expect(`dict.get [a=1] 'a' 0`).toEvaluateTo(1, globals)
await expect(`dict.get [a=1] 'b' 99`).toEvaluateTo(99, globals)
})
test('dict.set sets value', async () => {
await expect(`map = [a=1]; dict.set map 'b' 99; map.b`).toEvaluateTo(99, globals)
await expect(`map = [a=1]; dict.set map 'a' 100; map.a`).toEvaluateTo(100, globals)
})
test('dict.empty? checks if dict is empty', async () => {
await expect(`dict.empty? [=]`).toEvaluateTo(true, globals)
await expect(`dict.empty? [a=1]`).toEvaluateTo(false, globals)
})
test('dict.merge combines dicts', async () => {
await expect(`dict.merge [a=1] [b=2]`).toEvaluateTo({ a: 1, b: 2 }, globals)
})
test('dict.map transforms values', async () => {
await expect(`
double = do v k: v * 2 end
dict.map [a=1 b=2] double
`).toEvaluateTo({ a: 2, b: 4 }, globals)
})
test('dict.filter keeps matching entries', async () => {
await expect(`
gt-one = do v k: v > 1 end
dict.filter [a=1 b=2 c=3] gt-one
`).toEvaluateTo({ b: 2, c: 3 }, globals)
})
test('dict.from-entries creates dict from array', async () => {
await expect(`dict.from-entries [['a' 1] ['b' 2]]`).toEvaluateTo({ a: 1, b: 2 }, globals)
})
})
describe('math operations', () => {
test('math.abs returns absolute value', async () => {
await expect(`math.abs -5`).toEvaluateTo(5, globals)
await expect(`math.abs 5`).toEvaluateTo(5, globals)
})
test('math.floor rounds down', async () => {
await expect(`math.floor 3.7`).toEvaluateTo(3, globals)
})
test('math.ceil rounds up', async () => {
await expect(`math.ceil 3.2`).toEvaluateTo(4, globals)
})
test('math.round rounds to nearest', async () => {
await expect(`math.round 3.4`).toEvaluateTo(3, globals)
await expect(`math.round 3.6`).toEvaluateTo(4, globals)
})
test('math.min returns minimum', async () => {
await expect(`math.min 5 2 8 1`).toEvaluateTo(1, globals)
})
test('math.max returns maximum', async () => {
await expect(`math.max 5 2 8 1`).toEvaluateTo(8, globals)
})
test('math.pow computes power', async () => {
await expect(`math.pow 2 3`).toEvaluateTo(8, globals)
})
test('math.sqrt computes square root', async () => {
await expect(`math.sqrt 16`).toEvaluateTo(4, globals)
})
test('math.even? checks if even', async () => {
await expect(`math.even? 4`).toEvaluateTo(true, globals)
await expect(`math.even? 5`).toEvaluateTo(false, globals)
})
test('math.odd? checks if odd', async () => {
await expect(`math.odd? 5`).toEvaluateTo(true, globals)
await expect(`math.odd? 4`).toEvaluateTo(false, globals)
})
test('math.positive? checks if positive', async () => {
await expect(`math.positive? 5`).toEvaluateTo(true, globals)
await expect(`math.positive? -5`).toEvaluateTo(false, globals)
await expect(`math.positive? 0`).toEvaluateTo(false, globals)
})
test('math.negative? checks if negative', async () => {
await expect(`math.negative? -5`).toEvaluateTo(true, globals)
await expect(`math.negative? 5`).toEvaluateTo(false, globals)
})
test('math.zero? checks if zero', async () => {
await expect(`math.zero? 0`).toEvaluateTo(true, globals)
await expect(`math.zero? 5`).toEvaluateTo(false, globals)
})
test('math.clamp restricts value to range', async () => {
await expect(`math.clamp 5 0 10`).toEvaluateTo(5, globals)
await expect(`math.clamp -5 0 10`).toEvaluateTo(0, globals)
await expect(`math.clamp 15 0 10`).toEvaluateTo(10, globals)
})
test('math.sign returns sign of number', async () => {
await expect(`math.sign 5`).toEvaluateTo(1, globals)
await expect(`math.sign -5`).toEvaluateTo(-1, globals)
await expect(`math.sign 0`).toEvaluateTo(0, globals)
})
test('math.trunc truncates decimal', async () => {
await expect(`math.trunc 3.7`).toEvaluateTo(3, globals)
await expect(`math.trunc -3.7`).toEvaluateTo(-3, globals)
})
})
// describe('echo', () => {
// test('echo returns null value', async () => {
// await expect(`echo 'hello' 'world'`).toEvaluateTo(null, globalFunctions)
// })
// test('echo with array', async () => {
// await expect(`echo [1 2 3]`).toEvaluateTo(null, globalFunctions)
// })
// test('echo with multiple arguments', async () => {
// await expect(`echo 'test' 42 true`).toEvaluateTo(null, globalFunctions)
// })
// })

View File

@ -1,5 +1,6 @@
import { expect } from 'bun:test'
import { parser } from '#parser/shrimp'
import { setGlobals } from '#parser/tokenizer'
import { $ } from 'bun'
import { assert, errorMessage } from '#utils/utils'
import { Compiler } from '#compiler/compiler'
@ -30,18 +31,19 @@ await regenerateParser()
// Type declaration for TypeScript
declare module 'bun:test' {
interface Matchers<T> {
toMatchTree(expected: string): T
toMatchTree(expected: string, globals?: Record<string, any>): T
toMatchExpression(expected: string): T
toFailParse(): T
toEvaluateTo(expected: unknown, nativeFunctions?: Record<string, Function>): Promise<T>
toEvaluateTo(expected: unknown, globals?: Record<string, any>): Promise<T>
toFailEvaluation(): Promise<T>
}
}
expect.extend({
toMatchTree(received: unknown, expected: string) {
toMatchTree(received: unknown, expected: string, globals?: Record<string, any>) {
assert(typeof received === 'string', 'toMatchTree can only be used with string values')
if (globals) setGlobals(Object.keys(globals))
const tree = parser.parse(received)
const actual = treeToString(tree, received)
const normalizedExpected = trimWhitespace(expected)
@ -58,7 +60,7 @@ expect.extend({
}
},
toFailParse(received: unknown) {
toFailParse(received) {
assert(typeof received === 'string', 'toFailParse can only be used with string values')
try {
@ -93,29 +95,23 @@ expect.extend({
}
},
async toEvaluateTo(
received: unknown,
expected: unknown,
nativeFunctions: Record<string, Function> = {}
) {
async toEvaluateTo(received: unknown, expected: unknown, globals: Record<string, any> = {}) {
assert(typeof received === 'string', 'toEvaluateTo can only be used with string values')
try {
if (globals) setGlobals(Object.keys(globals))
const compiler = new Compiler(received)
const result = await run(compiler.bytecode, nativeFunctions)
const result = await run(compiler.bytecode, globals)
let value = VMResultToValue(result)
// Just treat regex as strings for comparison purposes
if (expected instanceof RegExp) expected = String(expected)
if (value instanceof RegExp) value = String(value)
if (value === expected) {
return { pass: true }
} else {
expect(value).toEqual(expected)
return {
message: () => `Expected evaluation to be ${expected}, but got ${value}`,
pass: false,
}
pass: true,
}
} catch (error) {
return {
@ -125,7 +121,7 @@ expect.extend({
}
},
async toFailEvaluation(received: unknown) {
async toFailEvaluation(received) {
assert(typeof received === 'string', 'toFailEvaluation can only be used with string values')
try {

View File

@ -1,6 +1,5 @@
import { Tree, TreeCursor } from '@lezer/common'
import { assertNever } from '#utils/utils'
import { type Value } from 'reefvm'
import { type Value, fromValue } from 'reefvm'
export const treeToString = (tree: Tree, input: string): string => {
const lines: string[] = []
@ -35,27 +34,5 @@ export const treeToString = (tree: Tree, input: string): string => {
}
export const VMResultToValue = (result: Value): unknown => {
if (
result.type === 'number' ||
result.type === 'boolean' ||
result.type === 'string' ||
result.type === 'regex'
) {
return result.value
} else if (result.type === 'null') {
return null
} else if (result.type === 'array') {
return result.value.map(VMResultToValue)
} else if (result.type === 'dict') {
const obj: Record<string, unknown> = {}
for (const [key, val] of Object.entries(result.value)) {
obj[key] = VMResultToValue(val)
}
return obj
} else if (result.type === 'function') {
return Function
} else {
assertNever(result)
}
return result.type === 'function' ? Function : fromValue(result)
}