Compare commits

..

24 Commits

Author SHA1 Message Date
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
16 changed files with 942 additions and 299 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. **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. **EOF handling**: The grammar uses `(statement | newlineOrSemicolon)+ eof?` to handle empty lines and end-of-file without infinite loops.
## Compiler Architecture ## Compiler Architecture

View File

@ -42,13 +42,13 @@ a-file = file.txt
3 3
# symbols can be assigned to functions. The body of the function comes after a colon `:` # 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 add 1 2
--- ---
3 3
# Functions can have multiple lines, they are terminated with `end` # Functions can have multiple lines, they are terminated with `end`
sub = fn x y: sub = do x y:
x - y x - y
end end
@ -82,9 +82,25 @@ add 1 (sub 5 2)
4 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 # HOLD UP
- how do we handle arrays?
- how do we handle hashes?
- conditionals - conditionals
- loops - loops

View File

@ -3,9 +3,6 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module", "type": "module",
"workspaces": [
"packages/*"
],
"scripts": { "scripts": {
"dev": "bun generate-parser && bun --hot src/server/server.tsx", "dev": "bun generate-parser && bun --hot src/server/server.tsx",
"generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts", "generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts",

View File

@ -265,6 +265,9 @@ export class Compiler {
} }
case terms.FunctionCallOrIdentifier: { case terms.FunctionCallOrIdentifier: {
if (node.firstChild?.name === 'DotGet')
return this.#compileNode(node.firstChild, input)
return [['TRY_CALL', value]] return [['TRY_CALL', value]]
} }
@ -468,6 +471,44 @@ export class Compiler {
return instructions 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: default:
throw new CompilerError( throw new CompilerError(
`Compiler doesn't know how to handle a "${node.type.name}" node.`, `Compiler doesn't know how to handle a "${node.type.name}" node.`,

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

@ -191,6 +191,15 @@ EscapeSeq {
"\\" ("$" | "n" | "t" | "r" | "\\" | "'") "\\" ("$" | "n" | "t" | "r" | "\\" | "'")
} }
Dict {
"[=]" |
"[" newlineOrSemicolon* NamedArg (newlineOrSemicolon | NamedArg)* "]"
}
Array {
"[" newlineOrSemicolon* (expression (newlineOrSemicolon | expression)*)? "]"
}
// We need expressionWithoutIdentifier to avoid conflicts in consumeToTerminator. // We need expressionWithoutIdentifier to avoid conflicts in consumeToTerminator.
// Without this, when parsing "my-var" at statement level, the parser can't decide: // Without this, when parsing "my-var" at statement level, the parser can't decide:
// - ambiguousFunctionCall → FunctionCallOrIdentifier → Identifier // - ambiguousFunctionCall → FunctionCallOrIdentifier → Identifier
@ -200,7 +209,7 @@ EscapeSeq {
// to go through ambiguousFunctionCall (which is what we want semantically). // 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. // Yes, it is annoying and I gave up trying to use GLR to fix it.
expressionWithoutIdentifier { 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 { block {

View File

@ -31,19 +31,21 @@ export const
EscapeSeq = 29, EscapeSeq = 29,
Boolean = 30, Boolean = 30,
Regex = 31, Regex = 31,
Null = 32, Dict = 32,
ConditionalOp = 33, NamedArg = 33,
FunctionDef = 34, NamedArgPrefix = 34,
Params = 35, FunctionDef = 35,
colon = 36, Params = 36,
keyword = 50, colon = 37,
PositionalArg = 38, keyword = 52,
Underscore = 39, Underscore = 39,
NamedArg = 40, Array = 40,
NamedArgPrefix = 41, Null = 41,
IfExpr = 43, ConditionalOp = 42,
SingleLineThenBlock = 45, PositionalArg = 43,
ThenBlock = 46, IfExpr = 45,
ElseIfExpr = 47, SingleLineThenBlock = 47,
ElseExpr = 49, ThenBlock = 48,
Assign = 51 ElseIfExpr = 49,
ElseExpr = 51,
Assign = 53

View File

@ -4,24 +4,24 @@ import {operatorTokenizer} from "./operatorTokenizer"
import {tokenizer, specializeKeyword} from "./tokenizer" import {tokenizer, specializeKeyword} from "./tokenizer"
import {trackScope} from "./scopeTracker" import {trackScope} from "./scopeTracker"
import {highlighting} from "./highlight" 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:76, null:82, if:92, elseif:100, else:104}
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, 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", states: "2YQYQbOOO!ZOpO'#CqO#mQcO'#CtO$gOSO'#CvO$uQbO'#ETOOQ`'#DP'#DPOOQa'#C|'#C|O%xQbO'#DUO&}QcO'#DxOOQa'#Dx'#DxO(TQcO'#DwO(lQRO'#CuO(zQcO'#DsO)cQbO'#CsOOQ`'#Dt'#DtO*ZQbO'#DsO*iQbO'#EZOOQ`'#DZ'#DZO+^QRO'#DcOOQ`'#Ds'#DsO+cQQO'#DrOOQ`'#Dr'#DrOOQ`'#Dd'#DdQYQbOOO+kObO,59]O+sQbO'#C}OOQa'#Dw'#DwOOQ`'#DX'#DXOOQ`'#EY'#EYOOQ`'#Dk'#DkO+}QbO,59[O,bQbO'#CxO,jQWO'#CyOOOO'#Dz'#DzOOOO'#De'#DeO-OOSO,59bOOQa,59b,59bOOQ`'#Dg'#DgO-^QbO'#DQO-fQQO,5:oOOQ`'#Df'#DfO-kQbO,59pO-rQQO,59hOOQa,59p,59pO-}QbO,59pO*iQbO,59aO*iQbO,59aO.XQRO,59_O/nQRO'#CuO0OQRO,59_O0[QQO,59_O0aQQO,59_O0iQbO'#DlO0tQbO,59ZO1VQRO,5:uO1^QQO,5:uO1cQbO,59}OOQ`,5:^,5:^OOQ`-E7b-E7bOOQa1G.w1G.wOOQ`,59i,59iOOQ`-E7i-E7iOOOO,59d,59dOOOO,59e,59eOOOO-E7c-E7cOOQa1G.|1G.|OOQ`-E7e-E7eO1mQbO1G0ZOOQ`-E7d-E7dO1zQQO1G/SOOQa1G/[1G/[O2VQbO1G/[OOQO'#Di'#DiO1zQQO1G/SOOQa1G/S1G/SOOQ`'#Dj'#DjO2VQbO1G/[OOQa1G.{1G.{O2aQcO1G.{OOQa1G.y1G.yO*iQbO,59rO*iQbO,59rO!`QbO'#CtO&PQbO'#CpOOQ`,5:W,5:WOOQ`-E7j-E7jO2{QbO1G0aOOQ`1G/i1G/iO3YQbO7+%uO3_QbO7+%vO3oQQO7+$nOOQa7+$n7+$nO3zQbO7+$vOOQa7+$v7+$vOOQO-E7g-E7gOOQ`-E7h-E7hOOQO1G/^1G/^O4UQRO1G/^OOQ`'#D]'#D]O4`QbO7+%{O4eQbO7+%|OOQ`<<Ia<<IaOOQ`'#Dh'#DhO4{QQO'#DhO5QQbO'#EVO5hQbO<<IbOOQa<<HY<<HYOOQa<<Hb<<HbOOQ`<<Ig<<IgOOQ`'#D^'#D^O5mQbO<<IhOOQ`,5:S,5:SOOQ`-E7f-E7fOOQ`AN>|AN>|O*iQbO'#D_OOQ`'#Dm'#DmO5xQbOAN?SO6TQQO'#DaOOQ`AN?SAN?SO6YQbOAN?SO6_QRO,59yO6fQQO,59yOOQ`-E7k-E7kOOQ`G24nG24nO6kQbOG24nO6pQQO,59{O6uQQO1G/eOOQ`LD*YLD*YO3_QbO1G/gO4eQbO7+%POOQ`7+%R7+%ROOQ`<<Hk<<Hk",
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~", stateData: "6}~O!dOS!eOS~O]QO^bO_XO`POaSOfXOnXOoXOyXO!O`O!j]O!mRO!tUO!uVO!veO~O!ihO~O]jO_XO`POaSOfXOnXOoXOriOwkOyXO!j]O!mRO!tUO!uVO|hX!vhX#PhX!{hXvhX~OP!kXQ!kXR!kXS!kXT!kXU!kXV!kXW!kXX!kXY!kXZ!kX[!kX~P!`OkqO!mtO!ooO!ppO~O]uOutP~O]jO_XO`POfXOnXOoXOriOyXO!j]O!mRO!tUO!uVO!vxO~O!z{O~P$}O]jO_XO`POaSOfXOnXOoXOriOwkOyXO!j]O!mRO!tUO!uVO~OP!lXQ!lXR!lXS!lX!v!lX#P!lXT!lXU!lXV!lXW!lXX!lXY!lXZ!lX[!lX!{!lXv!lX~P&POP!kXQ!kXR!kXS!kX!v!gX#P!gXv!gX~OP}OQ}OR!OOS!OO~OP}OQ}OR!OOS!OO!v!gX#P!gXv!gX~O]QO_XO`POaSOfXOnXOoXOyXO!j]O!mRO!tUO!uVO~O|!UO!v!gX#P!gXv!gX~O]jO_XO`POfXOnXOoXOyXO!j]O!mRO!tUO!uVO~OV!YO~O!v!ZO#P!ZO~O]!]Of!]O~OaSOw!^O~P*iO|da!vda#Pda!{davda~P&PO]!`O!j]O~O!m!aO!o!aO!p!aO!q!aO!r!aO!s!aO~OkqO!m!cO!ooO!ppO~O]uOutX~Ou!eO~O!z!hO~P$}OriO!v!jO!z!lO~O!v!mO!z!hO~P*iO!{!qOP!kXQ!kXR!kXS!kXT!kXU!kXV!kXW!kXX!kXY!kXZ!kX[!kX~OT!sOU!sOV!rOW!rOX!rOY!rOZ!rO[!rO~OP}OQ}OR!OOS!OO~P/SOP}OQ}OR!OOS!OO!{!qO~O|!UO!{!qO~O]!tO`PO!j]O~O|!UO!vca#Pca!{cavca~Ou!xO~P/SOu!xO~O^bO!O`O~P)cO^bO!O`O!v!{O~P)cOriO!v!jO!z!}O~O!v!mO!z#PO~P*iOP}OQ}ORiiSii!vii#Pii!{iivii~O^bO!O`O!v#WO~P)cOv#XO~O^bO!O`O!v#YOv!yP~P)cOriO!v!jO!z#^O~O!v!mO!z#_O~P*iO!{ziuzi~P/SOv#`O~O^bO!O`O!v#YOv!yP!S!yP!U!yP~P)cO!v#cO~O^bO!O`O!v#YOv!yX!S!yX!U!yX~P)cOv#eO~Ov#jO!S#fO!U#iO~Ov#oO!S#fO!U#iO~Ou#qO~Ov#oO~Ou#rO~P/SOu#rO~Ov#sO~O!v#tO~O!v#uO~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#[#]", goto: ".]#PPPPPPPPPPPPPPPPPPPP#Q#a#oP$e#a%^%sP&d&dPP%s&hP&{'fPPP%sP'i'uP'|P(Y(](fP(jP'|(p(v(|)S)Y)c)m)w*Q*XPPPP*_*c*wPP+Z,dP-XPPPPPPPP-]-]-pPP-x.P.PdcOg!Y!e!x!{#W#[#t#uR!S]i^O]g!U!Y!e!x!{#W#[#t#ufQO]g!Y!e!x!{#W#[#t#utjQVW`iny|}!O!i!n!r!s!t!u#O#fR!t!UfWO]g!Y!e!x!{#W#[#t#utXQVW`iny|}!O!i!n!r!s!t!u#O#fQ!`oR!u!Ud[Og!Y!e!x!{#W#[#t#uQ!R]Q!o}R!p!O!]XOQVW]`giny|}!O!Y!e!i!n!r!s!t!u!x!{#O#W#[#f#t#uTqRsYlQWn!t!uQzVQ!gyX!jz!g!k!|dcOg!Y!e!x!{#W#[#t#uYkQWn!t!uQ!S]R!^iRwSQ!S]Q!X`Q#S!sR#m#fZlQWn!t!uecOg!Y!e!x!{#W#[#t#uR#V!xQ#b#WQ#v#tR#w#uT#g#b#hQ#k#bR#p#hQgOR![gQsRR!bsQyVR!fyQvSR!dvW#[!{#W#t#uR#d#[Q!kzQ!|!gT#Q!k!|Q!n|Q#O!iT#R!n#OWnQW!t!uR!_nS!V_!TR!w!VQ#h#bR#n#hTfOgSdOgQ!y!YQ!z!eQ#U!xZ#Z!{#W#[#t#ud_Og!Y!e!x!{#W#[#t#uQ!T]R!v!UdZOg!Y!e!x!{#W#[#t#uYkQWn!t!uQ|VQ!Q]Q!W`Q!^iQ!iyW!m|!i!n#OQ!o}Q!p!OQ#S!rQ#T!sR#l#fdYOg!Y!e!x!{#W#[#t#utjQVW`iny|}!O!i!n!r!s!t!u#O#fR!P]TrRssTOQW]gin!Y!e!t!u!x!{#W#[#t#uQ#]!{V#a#W#t#uZmQWn!t!ueaOg!Y!e!x!{#W#[#t#u",
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", 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 Dict NamedArg NamedArgPrefix FunctionDef Params colon keyword Underscore Array Null ConditionalOp PositionalArg operator IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
maxTerm: 85, maxTerm: 93,
context: trackScope, context: trackScope,
nodeProps: [ nodeProps: [
["closedBy", 36,"end"] ["closedBy", 37,"end"]
], ],
propSources: [highlighting], propSources: [highlighting],
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 7, repeatNodeCount: 10,
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~", 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$QUkSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUkS!dYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UkS!vQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZkS!eYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!eYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O!o~~'aO!m~U'hUkS!jQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUkS!{QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWkSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZYkSfQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OWkSOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oWkSfQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^WkSOt#{uw#{x!P#{!P!Q+v!Q#O#{#P;'S#{;'S;=`$d<%lO#{U+{^kSOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q#{!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wU-O^kSoQOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q0o!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wQ.PXoQOY-zZ!P-z!P!Q.l!Q!}-z!}#O/Z#O#P0Y#P;'S-z;'S;=`0i<%lO-zQ.oP!P!Q.rQ.wUoQ#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-zU0tWkSOt#{uw#{x!P#{!P!Q1^!Q#O#{#P;'S#{;'S;=`$d<%lO#{U1ebkSoQOt#{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[kSOY2mYZ#{Zt2mtu/Zuw2mwx/Zx#O2m#O#P/s#P#Q,w#Q;'S2m;'S;=`3h<%lO2mU3kP;=`<%l2mU3qP;=`<%l,wU3{UkSuQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4fW!uQkSOt#{uw#{x!_#{!_!`5O!`#O#{#P;'S#{;'S;=`$d<%lO#{U5TVkSOt#{uw#{x#O#{#P#Q5j#Q;'S#{;'S;=`$d<%lO#{U5qU!tQkSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6YO!p~U6aU!zQkSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6zUkSwQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7cYkSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{U8YUrQkSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U8qZkSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#U9d#U#o7^#o;'S#{;'S;=`$d<%lO#{U9i[kSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#`7^#`#a:_#a#o7^#o;'S#{;'S;=`$d<%lO#{U:d[kSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#g7^#g#h;Y#h#o7^#o;'S#{;'S;=`$d<%lO#{U;_[kSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#X7^#X#Y<T#Y#o7^#o;'S#{;'S;=`$d<%lO#{U<[YnQkSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=RY!qWkSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=xY!sWkSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^>o[!rWkSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#f7^#f#g?e#g#o7^#o;'S#{;'S;=`$d<%lO#{U?j[kSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#i7^#i#j;Y#j#o7^#o;'S#{;'S;=`$d<%lO#{U@gU|QkSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~AOO#P~",
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!d~~", 11)], tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!i~~", 11)],
topRules: {"Program":[0,18]}, 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}], 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 tokenPrec: 1008
}) })

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

@ -195,7 +195,13 @@ const isWhiteSpace = (ch: number): boolean => {
} }
const isWordChar = (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 => { const isLowercaseLetter = (ch: number): boolean => {

View File

@ -75,10 +75,11 @@ export const globalFunctions = {
}, },
each: async (list: any[], cb: Function) => { each: async (list: any[], cb: Function) => {
for (const value of list) await cb(value) for (const value of list) await cb(value)
return list
}, },
// modules // modules
use: async function (this: VM, path: string) { load: async function (this: VM, path: string): Promise<Record<string, Value>> {
const scope = this.scope const scope = this.scope
const pc = this.pc const pc = this.pc
@ -92,16 +93,16 @@ export const globalFunctions = {
await this.continue() await this.continue()
const module: Map<string, Value> = new Map const module: Record<string, Value> = {}
for (const [name, value] of this.scope.locals.entries()) for (const [name, value] of this.scope.locals.entries())
module.set(name, value) module[name] = value
this.scope = scope this.scope = scope
this.pc = pc this.pc = pc
this.stopped = false this.stopped = false
this.scope.set(parse(fullPath).name, { type: 'dict', value: module }) return module
} },
} }
export function formatValue(value: Value, inner = false): string { export function formatValue(value: Value, inner = false): string {

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

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { parser } from '#parser/shrimp'
import { $ } from 'bun' import { $ } from 'bun'
import { assert, errorMessage } from '#utils/utils' import { assert, errorMessage } from '#utils/utils'
import { Compiler } from '#compiler/compiler' import { Compiler } from '#compiler/compiler'
import { run, VM } from 'reefvm' import { run, VM, type TypeScriptFunction } from 'reefvm'
import { treeToString, VMResultToValue } from '#utils/tree' import { treeToString, VMResultToValue } from '#utils/tree'
const regenerateParser = async () => { const regenerateParser = async () => {
@ -93,11 +93,7 @@ expect.extend({
} }
}, },
async toEvaluateTo( async toEvaluateTo(received: unknown, expected: unknown, globals: Record<string, any> = {}) {
received: unknown,
expected: unknown,
globals: Record<string, any> = {}
) {
assert(typeof received === 'string', 'toEvaluateTo can only be used with string values') assert(typeof received === 'string', 'toEvaluateTo can only be used with string values')
try { try {
@ -109,11 +105,14 @@ expect.extend({
if (expected instanceof RegExp) expected = String(expected) if (expected instanceof RegExp) expected = String(expected)
if (value instanceof RegExp) value = String(value) if (value instanceof RegExp) value = String(value)
if (value === expected) { if (isEqual(value, expected)) {
return { pass: true } return { pass: true }
} else { } else {
return { return {
message: () => `Expected evaluation to be ${expected}, but got ${value}`, message: () =>
`Expected evaluation to be ${JSON.stringify(expected)}, but got ${JSON.stringify(
value
)}`,
pass: false, pass: false,
} }
} }
@ -165,3 +164,29 @@ const trimWhitespace = (str: string): string => {
}) })
.join('\n') .join('\n')
} }
function isEqual(a: any, b: any): boolean {
if (a === null && b === null) return true
switch (typeof a) {
case 'string':
case 'number':
case 'boolean':
case 'undefined':
return a === b
default:
return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b))
}
}
function sortKeys(o: any): any {
if (Array.isArray(o)) return o.map(sortKeys)
if (o && typeof o === 'object' && o.constructor === Object)
return Object.keys(o)
.sort()
.reduce((r, k) => {
r[k] = sortKeys(o[k])
return r
}, {} as any)
return o
}

View File

@ -1,6 +1,6 @@
import { Tree, TreeCursor } from '@lezer/common' import { Tree, TreeCursor } from '@lezer/common'
import { assertNever } from '#utils/utils' import { assertNever } from '#utils/utils'
import { type Value } from 'reefvm' import { type Value, fromValue } from 'reefvm'
export const treeToString = (tree: Tree, input: string): string => { export const treeToString = (tree: Tree, input: string): string => {
const lines: string[] = [] const lines: string[] = []
@ -35,27 +35,6 @@ export const treeToString = (tree: Tree, input: string): string => {
} }
export const VMResultToValue = (result: Value): unknown => { export const VMResultToValue = (result: Value): unknown => {
if ( if (result.type === 'function') return Function
result.type === 'number' || else return fromValue(result)
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)
}
} }