Compare commits
24 Commits
de30d85304
...
0eca3685f5
| Author | SHA1 | Date | |
|---|---|---|---|
| 0eca3685f5 | |||
|
|
dd2edb6dda | ||
| b738e6cfd1 | |||
| bf1196bf96 | |||
| f25ec024c2 | |||
| 6d19896d1a | |||
| f08b16824a | |||
| e1ba9c630d | |||
| b03610761b | |||
| b46154f753 | |||
| 3a04970dca | |||
| 2ff4615aab | |||
| 7387c56a20 | |||
| d3e83e17b2 | |||
| 9345c743ff | |||
| ee4de6c59e | |||
| 35e6b63499 | |||
| 62036b1e4b | |||
| 1aa1570135 | |||
| 8112515278 | |||
| 982054eb54 | |||
| 34c1177636 | |||
| 339c09eb8c | |||
| 7da4c14962 |
13
CLAUDE.md
13
CLAUDE.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -3,9 +3,6 @@
|
|||
"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",
|
||||
|
|
|
|||
|
|
@ -265,6 +265,9 @@ export class Compiler {
|
|||
}
|
||||
|
||||
case terms.FunctionCallOrIdentifier: {
|
||||
if (node.firstChild?.name === 'DotGet')
|
||||
return this.#compileNode(node.firstChild, input)
|
||||
|
||||
return [['TRY_CALL', value]]
|
||||
}
|
||||
|
||||
|
|
@ -468,6 +471,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.`,
|
||||
|
|
|
|||
157
src/compiler/tests/literals.test.ts
Normal file
157
src/compiler/tests/literals.test.ts
Normal 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 })
|
||||
})
|
||||
})
|
||||
|
|
@ -191,6 +191,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 +209,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 {
|
||||
|
|
|
|||
|
|
@ -31,19 +31,21 @@ export const
|
|||
EscapeSeq = 29,
|
||||
Boolean = 30,
|
||||
Regex = 31,
|
||||
Null = 32,
|
||||
ConditionalOp = 33,
|
||||
FunctionDef = 34,
|
||||
Params = 35,
|
||||
colon = 36,
|
||||
keyword = 50,
|
||||
PositionalArg = 38,
|
||||
Dict = 32,
|
||||
NamedArg = 33,
|
||||
NamedArgPrefix = 34,
|
||||
FunctionDef = 35,
|
||||
Params = 36,
|
||||
colon = 37,
|
||||
keyword = 52,
|
||||
Underscore = 39,
|
||||
NamedArg = 40,
|
||||
NamedArgPrefix = 41,
|
||||
IfExpr = 43,
|
||||
SingleLineThenBlock = 45,
|
||||
ThenBlock = 46,
|
||||
ElseIfExpr = 47,
|
||||
ElseExpr = 49,
|
||||
Assign = 51
|
||||
Array = 40,
|
||||
Null = 41,
|
||||
ConditionalOp = 42,
|
||||
PositionalArg = 43,
|
||||
IfExpr = 45,
|
||||
SingleLineThenBlock = 47,
|
||||
ThenBlock = 48,
|
||||
ElseIfExpr = 49,
|
||||
ElseExpr = 51,
|
||||
Assign = 53
|
||||
|
|
|
|||
|
|
@ -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:76, null:82, if:92, elseif:100, else:104}
|
||||
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: "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: "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: ".]#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 Dict NamedArg NamedArgPrefix FunctionDef Params colon keyword Underscore Array Null ConditionalOp PositionalArg operator IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
|
||||
maxTerm: 93,
|
||||
context: trackScope,
|
||||
nodeProps: [
|
||||
["closedBy", 36,"end"]
|
||||
["closedBy", 37,"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)],
|
||||
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$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!i~~", 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
|
||||
tokenPrec: 1008
|
||||
})
|
||||
|
|
|
|||
492
src/parser/tests/literals.test.ts
Normal file
492
src/parser/tests/literals.test.ts
Normal 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
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
@ -195,7 +195,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 => {
|
||||
|
|
|
|||
|
|
@ -75,10 +75,11 @@ export const globalFunctions = {
|
|||
},
|
||||
each: async (list: any[], cb: Function) => {
|
||||
for (const value of list) await cb(value)
|
||||
return list
|
||||
},
|
||||
|
||||
// modules
|
||||
use: async function (this: VM, path: string) {
|
||||
load: async function (this: VM, path: string): Promise<Record<string, Value>> {
|
||||
const scope = this.scope
|
||||
const pc = this.pc
|
||||
|
||||
|
|
@ -92,16 +93,16 @@ export const globalFunctions = {
|
|||
|
||||
await this.continue()
|
||||
|
||||
const module: Map<string, Value> = new Map
|
||||
const module: Record<string, Value> = {}
|
||||
for (const [name, value] of this.scope.locals.entries())
|
||||
module.set(name, value)
|
||||
module[name] = value
|
||||
|
||||
this.scope = scope
|
||||
this.pc = pc
|
||||
this.stopped = false
|
||||
|
||||
this.scope.set(parse(fullPath).name, { type: 'dict', value: module })
|
||||
}
|
||||
return module
|
||||
},
|
||||
}
|
||||
|
||||
export function formatValue(value: Value, inner = false): string {
|
||||
|
|
|
|||
42
src/prelude/tests/load.ts
Normal file
42
src/prelude/tests/load.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,256 +1,147 @@
|
|||
import { expect, describe, test, mock } from 'bun:test'
|
||||
import { globalFunctions, formatValue } from '#prelude'
|
||||
import { toValue } from 'reefvm'
|
||||
import { expect, describe, test } from 'bun:test'
|
||||
import { globalFunctions } from '#prelude'
|
||||
|
||||
describe('string operations', () => {
|
||||
test('to-upper converts to uppercase', () => {
|
||||
expect(globalFunctions['to-upper']('hello')).toBe('HELLO')
|
||||
expect(globalFunctions['to-upper']('Hello World!')).toBe('HELLO WORLD!')
|
||||
test('to-upper converts to uppercase', async () => {
|
||||
await expect(`to-upper 'hello'`).toEvaluateTo('HELLO', globalFunctions)
|
||||
await expect(`to-upper 'Hello World!'`).toEvaluateTo('HELLO WORLD!', globalFunctions)
|
||||
})
|
||||
|
||||
test('to-lower converts to lowercase', () => {
|
||||
expect(globalFunctions['to-lower']('HELLO')).toBe('hello')
|
||||
expect(globalFunctions['to-lower']('Hello World!')).toBe('hello world!')
|
||||
test('to-lower converts to lowercase', async () => {
|
||||
await expect(`to-lower 'HELLO'`).toEvaluateTo('hello', globalFunctions)
|
||||
await expect(`to-lower 'Hello World!'`).toEvaluateTo('hello world!', globalFunctions)
|
||||
})
|
||||
|
||||
test('trim removes whitespace', () => {
|
||||
expect(globalFunctions.trim(' hello ')).toBe('hello')
|
||||
expect(globalFunctions.trim('\n\thello\t\n')).toBe('hello')
|
||||
test('trim removes whitespace', async () => {
|
||||
await expect(`trim ' hello '`).toEvaluateTo('hello', globalFunctions)
|
||||
await expect(`trim '\\n\\thello\\t\\n'`).toEvaluateTo('hello', globalFunctions)
|
||||
})
|
||||
|
||||
test('split divides string by separator', () => {
|
||||
expect(globalFunctions.split('a,b,c', ',')).toEqual(['a', 'b', 'c'])
|
||||
expect(globalFunctions.split('hello', '')).toEqual(['h', 'e', 'l', 'l', 'o'])
|
||||
test('split divides string by separator', async () => {
|
||||
await expect(`split 'a,b,c' ','`).toEvaluateTo(['a', 'b', 'c'], globalFunctions)
|
||||
await expect(`split 'hello' ''`).toEvaluateTo(['h', 'e', 'l', 'l', 'o'], globalFunctions)
|
||||
})
|
||||
|
||||
test('split uses comma as default separator', () => {
|
||||
expect(globalFunctions.split('a,b,c')).toEqual(['a', 'b', 'c'])
|
||||
test('split with comma separator', async () => {
|
||||
await expect(`split 'a,b,c' ','`).toEvaluateTo(['a', 'b', 'c'], globalFunctions)
|
||||
})
|
||||
|
||||
test('join combines array elements', () => {
|
||||
expect(globalFunctions.join(['a', 'b', 'c'], '-')).toBe('a-b-c')
|
||||
expect(globalFunctions.join(['hello', 'world'], ' ')).toBe('hello world')
|
||||
test('join combines array elements', async () => {
|
||||
await expect(`join ['a' 'b' 'c'] '-'`).toEvaluateTo('a-b-c', globalFunctions)
|
||||
await expect(`join ['hello' 'world'] ' '`).toEvaluateTo('hello world', globalFunctions)
|
||||
})
|
||||
|
||||
test('join uses comma as default separator', () => {
|
||||
expect(globalFunctions.join(['a', 'b', 'c'])).toBe('a,b,c')
|
||||
test('join with comma separator', async () => {
|
||||
await expect(`join ['a' 'b' 'c'] ','`).toEvaluateTo('a,b,c', globalFunctions)
|
||||
})
|
||||
})
|
||||
|
||||
describe('introspection', () => {
|
||||
test('type returns proper types', () => {
|
||||
expect(globalFunctions.type(toValue('hello'))).toBe('string')
|
||||
expect(globalFunctions.type('hello')).toBe('string')
|
||||
|
||||
expect(globalFunctions.type(toValue(42))).toBe('number')
|
||||
expect(globalFunctions.type(42)).toBe('number')
|
||||
|
||||
expect(globalFunctions.type(toValue(true))).toBe('boolean')
|
||||
expect(globalFunctions.type(false)).toBe('boolean')
|
||||
|
||||
expect(globalFunctions.type(toValue(null))).toBe('null')
|
||||
|
||||
expect(globalFunctions.type(toValue([1, 2, 3]))).toBe('array')
|
||||
|
||||
const dict = new Map([['key', toValue('value')]])
|
||||
expect(globalFunctions.type({ type: 'dict', value: dict })).toBe('dict')
|
||||
test('type returns proper types', async () => {
|
||||
await expect(`type 'hello'`).toEvaluateTo('string', globalFunctions)
|
||||
await expect(`type 42`).toEvaluateTo('number', globalFunctions)
|
||||
await expect(`type true`).toEvaluateTo('boolean', globalFunctions)
|
||||
await expect(`type false`).toEvaluateTo('boolean', globalFunctions)
|
||||
await expect(`type null`).toEvaluateTo('null', globalFunctions)
|
||||
await expect(`type [1 2 3]`).toEvaluateTo('array', globalFunctions)
|
||||
await expect(`type [a=1 b=2]`).toEvaluateTo('dict', globalFunctions)
|
||||
})
|
||||
|
||||
test('length', () => {
|
||||
expect(globalFunctions.length(toValue('hello'))).toBe(5)
|
||||
expect(globalFunctions.length('hello')).toBe(5)
|
||||
|
||||
expect(globalFunctions.length(toValue([1, 2, 3]))).toBe(3)
|
||||
expect(globalFunctions.length([1, 2, 3])).toBe(3)
|
||||
|
||||
const dict = new Map([['a', toValue(1)], ['b', toValue(2)]])
|
||||
expect(globalFunctions.length({ type: 'dict', value: dict })).toBe(2)
|
||||
|
||||
expect(globalFunctions.length(toValue(42))).toBe(0)
|
||||
expect(globalFunctions.length(toValue(true))).toBe(0)
|
||||
expect(globalFunctions.length(toValue(null))).toBe(0)
|
||||
test('length', async () => {
|
||||
await expect(`length 'hello'`).toEvaluateTo(5, globalFunctions)
|
||||
await expect(`length [1 2 3]`).toEvaluateTo(3, globalFunctions)
|
||||
await expect(`length [a=1 b=2]`).toEvaluateTo(2, globalFunctions)
|
||||
await expect(`length 42`).toEvaluateTo(0, globalFunctions)
|
||||
await expect(`length true`).toEvaluateTo(0, globalFunctions)
|
||||
await expect(`length null`).toEvaluateTo(0, globalFunctions)
|
||||
})
|
||||
|
||||
test('inspect formats values', () => {
|
||||
const result = globalFunctions.inspect(toValue('hello'))
|
||||
expect(result).toContain('hello')
|
||||
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', globalFunctions)
|
||||
})
|
||||
})
|
||||
|
||||
describe('collections', () => {
|
||||
test('list creates array from arguments', () => {
|
||||
expect(globalFunctions.list(1, 2, 3)).toEqual([1, 2, 3])
|
||||
expect(globalFunctions.list('a', 'b')).toEqual(['a', 'b'])
|
||||
expect(globalFunctions.list()).toEqual([])
|
||||
test('list creates array from arguments', async () => {
|
||||
await expect(`list 1 2 3`).toEvaluateTo([1, 2, 3], globalFunctions)
|
||||
await expect(`list 'a' 'b'`).toEvaluateTo(['a', 'b'], globalFunctions)
|
||||
await expect(`list`).toEvaluateTo([], globalFunctions)
|
||||
})
|
||||
|
||||
test('dict creates object from named arguments', () => {
|
||||
expect(globalFunctions.dict({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 })
|
||||
expect(globalFunctions.dict()).toEqual({})
|
||||
test('dict creates object from named arguments', async () => {
|
||||
await expect(`dict a=1 b=2`).toEvaluateTo({ a: 1, b: 2 }, globalFunctions)
|
||||
await expect(`dict`).toEvaluateTo({}, globalFunctions)
|
||||
})
|
||||
|
||||
test('at retrieves element at index', () => {
|
||||
expect(globalFunctions.at([10, 20, 30], 0)).toBe(10)
|
||||
expect(globalFunctions.at([10, 20, 30], 2)).toBe(30)
|
||||
test('at retrieves element at index', async () => {
|
||||
await expect(`at [10 20 30] 0`).toEvaluateTo(10, globalFunctions)
|
||||
await expect(`at [10 20 30] 2`).toEvaluateTo(30, globalFunctions)
|
||||
})
|
||||
|
||||
test('at retrieves property from object', () => {
|
||||
expect(globalFunctions.at({ name: 'test' }, 'name')).toBe('test')
|
||||
test('at retrieves property from object', async () => {
|
||||
await expect(`at [name='test'] 'name'`).toEvaluateTo('test', globalFunctions)
|
||||
})
|
||||
|
||||
test('slice extracts array subset', () => {
|
||||
expect(globalFunctions.slice([1, 2, 3, 4, 5], 1, 3)).toEqual([2, 3])
|
||||
expect(globalFunctions.slice([1, 2, 3, 4, 5], 2)).toEqual([3, 4, 5])
|
||||
test('slice extracts array subset', async () => {
|
||||
await expect(`slice [1 2 3 4 5] 1 3`).toEvaluateTo([2, 3], globalFunctions)
|
||||
await expect(`slice [1 2 3 4 5] 2 5`).toEvaluateTo([3, 4, 5], globalFunctions)
|
||||
})
|
||||
|
||||
test('range creates number sequence', () => {
|
||||
expect(globalFunctions.range(0, 5)).toEqual([0, 1, 2, 3, 4, 5])
|
||||
expect(globalFunctions.range(3, 6)).toEqual([3, 4, 5, 6])
|
||||
test('range creates number sequence', async () => {
|
||||
await expect(`range 0 5`).toEvaluateTo([0, 1, 2, 3, 4, 5], globalFunctions)
|
||||
await expect(`range 3 6`).toEvaluateTo([3, 4, 5, 6], globalFunctions)
|
||||
})
|
||||
|
||||
test('range with single argument starts from 0', () => {
|
||||
expect(globalFunctions.range(3, null)).toEqual([0, 1, 2, 3])
|
||||
expect(globalFunctions.range(0, null)).toEqual([0])
|
||||
test('range with single argument starts from 0', async () => {
|
||||
await expect(`range 3 null`).toEvaluateTo([0, 1, 2, 3], globalFunctions)
|
||||
await expect(`range 0 null`).toEvaluateTo([0], globalFunctions)
|
||||
})
|
||||
})
|
||||
|
||||
describe('enumerables', () => {
|
||||
test('map transforms array elements', async () => {
|
||||
const double = (x: number) => x * 2
|
||||
const result = await globalFunctions.map([1, 2, 3], double)
|
||||
expect(result).toEqual([2, 4, 6])
|
||||
})
|
||||
|
||||
test('map works with async callbacks', async () => {
|
||||
const asyncDouble = async (x: number) => {
|
||||
await Promise.resolve()
|
||||
return x * 2
|
||||
}
|
||||
const result = await globalFunctions.map([1, 2, 3], asyncDouble)
|
||||
expect(result).toEqual([2, 4, 6])
|
||||
await expect(`
|
||||
double = do x: x * 2 end
|
||||
map [1 2 3] double
|
||||
`).toEvaluateTo([2, 4, 6], globalFunctions)
|
||||
})
|
||||
|
||||
test('map handles empty array', async () => {
|
||||
const fn = (x: number) => x * 2
|
||||
const result = await globalFunctions.map([], fn)
|
||||
expect(result).toEqual([])
|
||||
await expect(`
|
||||
double = do x: x * 2 end
|
||||
map [] double
|
||||
`).toEvaluateTo([], globalFunctions)
|
||||
})
|
||||
|
||||
test('each iterates over array', async () => {
|
||||
const results: number[] = []
|
||||
await globalFunctions.each([1, 2, 3], (x: number) => {
|
||||
results.push(x * 2)
|
||||
})
|
||||
expect(results).toEqual([2, 4, 6])
|
||||
})
|
||||
|
||||
test('each works with async callbacks', async () => {
|
||||
const results: number[] = []
|
||||
await globalFunctions.each([1, 2, 3], async (x: number) => {
|
||||
await Promise.resolve()
|
||||
results.push(x * 2)
|
||||
})
|
||||
expect(results).toEqual([2, 4, 6])
|
||||
// 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], globalFunctions)
|
||||
})
|
||||
|
||||
test('each handles empty array', async () => {
|
||||
let called = false
|
||||
await globalFunctions.each([], () => {
|
||||
called = true
|
||||
})
|
||||
expect(called).toBe(false)
|
||||
await expect(`
|
||||
fn = do x: x end
|
||||
each [] fn
|
||||
`).toEvaluateTo([], globalFunctions)
|
||||
})
|
||||
})
|
||||
|
||||
describe('echo', () => {
|
||||
test('echo logs arguments to console', () => {
|
||||
const spy = mock(() => { })
|
||||
const originalLog = console.log
|
||||
console.log = spy
|
||||
// describe('echo', () => {
|
||||
// test('echo returns null value', async () => {
|
||||
// await expect(`echo 'hello' 'world'`).toEvaluateTo(null, globalFunctions)
|
||||
// })
|
||||
|
||||
globalFunctions.echo('hello', 'world')
|
||||
// test('echo with array', async () => {
|
||||
// await expect(`echo [1 2 3]`).toEvaluateTo(null, globalFunctions)
|
||||
// })
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('hello', 'world')
|
||||
console.log = originalLog
|
||||
})
|
||||
|
||||
test('echo returns null value', () => {
|
||||
const originalLog = console.log
|
||||
console.log = () => { }
|
||||
|
||||
const result = globalFunctions.echo('test')
|
||||
|
||||
expect(result).toEqual(toValue(null))
|
||||
console.log = originalLog
|
||||
})
|
||||
|
||||
test('echo formats array values', () => {
|
||||
const spy = mock(() => { })
|
||||
const originalLog = console.log
|
||||
console.log = spy
|
||||
|
||||
globalFunctions.echo(toValue([1, 2, 3]))
|
||||
|
||||
// Should format the array, not just log the raw value
|
||||
expect(spy).toHaveBeenCalled()
|
||||
// @ts-ignore
|
||||
const logged = spy.mock.calls[0][0]
|
||||
// @ts-ignore
|
||||
expect(logged).toContain('list')
|
||||
|
||||
console.log = originalLog
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatValue', () => {
|
||||
test('formats string with quotes', () => {
|
||||
const result = formatValue(toValue('hello'))
|
||||
expect(result).toContain('hello')
|
||||
expect(result).toContain("'")
|
||||
})
|
||||
|
||||
test('formats numbers', () => {
|
||||
const result = formatValue(toValue(42))
|
||||
expect(result).toContain('42')
|
||||
})
|
||||
|
||||
test('formats booleans', () => {
|
||||
expect(formatValue(toValue(true))).toContain('true')
|
||||
expect(formatValue(toValue(false))).toContain('false')
|
||||
})
|
||||
|
||||
test('formats null', () => {
|
||||
const result = formatValue(toValue(null))
|
||||
expect(result).toContain('null')
|
||||
})
|
||||
|
||||
test('formats arrays', () => {
|
||||
const result = formatValue(toValue([1, 2, 3]))
|
||||
expect(result).toContain('list')
|
||||
})
|
||||
|
||||
test('formats nested arrays with parentheses', () => {
|
||||
const inner = toValue([1, 2])
|
||||
const outer = toValue([inner])
|
||||
const result = formatValue(outer)
|
||||
expect(result).toContain('list')
|
||||
expect(result).toContain('(')
|
||||
expect(result).toContain(')')
|
||||
})
|
||||
|
||||
test('formats dicts', () => {
|
||||
const dict = new Map([
|
||||
['name', toValue('test')],
|
||||
['age', toValue(42)]
|
||||
])
|
||||
const result = formatValue({ type: 'dict', value: dict })
|
||||
expect(result).toContain('dict')
|
||||
expect(result).toContain('name=')
|
||||
expect(result).toContain('age=')
|
||||
})
|
||||
|
||||
test('escapes single quotes in strings', () => {
|
||||
const result = formatValue(toValue("it's"))
|
||||
expect(result).toContain("\\'")
|
||||
})
|
||||
})
|
||||
// test('echo with multiple arguments', async () => {
|
||||
// await expect(`echo 'test' 42 true`).toEvaluateTo(null, globalFunctions)
|
||||
// })
|
||||
// })
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -3,7 +3,7 @@ import { parser } from '#parser/shrimp'
|
|||
import { $ } from 'bun'
|
||||
import { assert, errorMessage } from '#utils/utils'
|
||||
import { Compiler } from '#compiler/compiler'
|
||||
import { run, VM } from 'reefvm'
|
||||
import { run, VM, type TypeScriptFunction } from 'reefvm'
|
||||
import { treeToString, VMResultToValue } from '#utils/tree'
|
||||
|
||||
const regenerateParser = async () => {
|
||||
|
|
@ -93,11 +93,7 @@ expect.extend({
|
|||
}
|
||||
},
|
||||
|
||||
async toEvaluateTo(
|
||||
received: unknown,
|
||||
expected: unknown,
|
||||
globals: Record<string, any> = {}
|
||||
) {
|
||||
async toEvaluateTo(received: unknown, expected: unknown, globals: Record<string, any> = {}) {
|
||||
assert(typeof received === 'string', 'toEvaluateTo can only be used with string values')
|
||||
|
||||
try {
|
||||
|
|
@ -109,11 +105,14 @@ expect.extend({
|
|||
if (expected instanceof RegExp) expected = String(expected)
|
||||
if (value instanceof RegExp) value = String(value)
|
||||
|
||||
if (value === expected) {
|
||||
if (isEqual(value, expected)) {
|
||||
return { pass: true }
|
||||
} else {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
@ -165,3 +164,29 @@ const trimWhitespace = (str: string): string => {
|
|||
})
|
||||
.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 +35,6 @@ 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)
|
||||
}
|
||||
if (result.type === 'function') return Function
|
||||
else return fromValue(result)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user