Compare commits

...

10 Commits

12 changed files with 867 additions and 109 deletions

View File

@ -1,6 +1,7 @@
import { CompilerError } from '#compiler/compilerError.ts' import { CompilerError } from '#compiler/compilerError.ts'
import { parser } from '#parser/shrimp.ts' import { parser } from '#parser/shrimp.ts'
import * as terms from '#parser/shrimp.terms' import * as terms from '#parser/shrimp.terms'
import { setGlobals } from '#parser/tokenizer'
import type { SyntaxNode, Tree } from '@lezer/common' import type { SyntaxNode, Tree } from '@lezer/common'
import { assert, errorMessage } from '#utils/utils' import { assert, errorMessage } from '#utils/utils'
import { toBytecode, type Bytecode, type ProgramItem, bytecodeToString } from 'reefvm' import { toBytecode, type Bytecode, type ProgramItem, bytecodeToString } from 'reefvm'
@ -53,8 +54,9 @@ export class Compiler {
bytecode: Bytecode bytecode: Bytecode
pipeCounter = 0 pipeCounter = 0
constructor(public input: string) { constructor(public input: string, globals?: string[]) {
try { try {
if (globals) setGlobals(globals)
const cst = parser.parse(input) const cst = parser.parse(input)
const errors = checkTreeForErrors(cst) const errors = checkTreeForErrors(cst)
@ -218,6 +220,9 @@ export class Compiler {
case '/': case '/':
instructions.push(['DIV']) instructions.push(['DIV'])
break break
case '%':
instructions.push(['MOD'])
break
default: default:
throw new CompilerError(`Unsupported binary operator: ${opValue}`, op.from, op.to) throw new CompilerError(`Unsupported binary operator: ${opValue}`, op.from, op.to)
} }
@ -361,7 +366,7 @@ export class Compiler {
const opValue = input.slice(op.from, op.to) const opValue = input.slice(op.from, op.to)
switch (opValue) { switch (opValue) {
case '=': case '==':
instructions.push(...leftInstructions, ...rightInstructions, ['EQ']) instructions.push(...leftInstructions, ...rightInstructions, ['EQ'])
break break

View File

@ -38,6 +38,12 @@ describe('compiler', () => {
expect('15 / 3').toEvaluateTo(5) expect('15 / 3').toEvaluateTo(5)
}) })
test('modulo', () => {
expect('44 % 2').toEvaluateTo(0)
expect('44 % 3').toEvaluateTo(2)
expect('3 % 4').toEvaluateTo(3)
})
test('assign number', () => { test('assign number', () => {
expect('x = 5').toEvaluateTo(5) expect('x = 5').toEvaluateTo(5)
}) })
@ -105,7 +111,7 @@ describe('compiler', () => {
expect(`(10 > 20)`).toEvaluateTo(false) expect(`(10 > 20)`).toEvaluateTo(false)
expect(`(4 <= 9)`).toEvaluateTo(true) expect(`(4 <= 9)`).toEvaluateTo(true)
expect(`(15 >= 20)`).toEvaluateTo(false) expect(`(15 >= 20)`).toEvaluateTo(false)
expect(`(7 = 7)`).toEvaluateTo(true) expect(`(7 == 7)`).toEvaluateTo(true)
expect(`(5 != 5)`).toEvaluateTo(false) expect(`(5 != 5)`).toEvaluateTo(false)
expect(`('shave' and 'haircut')`).toEvaluateTo('haircut') expect(`('shave' and 'haircut')`).toEvaluateTo('haircut')
expect(`(false and witness)`).toEvaluateTo(false) expect(`(false and witness)`).toEvaluateTo(false)

View File

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

View File

@ -6,7 +6,7 @@
@top Program { item* } @top Program { item* }
@external tokens operatorTokenizer from "./operatorTokenizer" { Star, Slash, Plus, Minus, And, Or, Eq, Neq, Lt, Lte, Gt, Gte } @external tokens operatorTokenizer from "./operatorTokenizer" { Star, Slash, Plus, Minus, And, Or, Eq, EqEq, Neq, Lt, Lte, Gt, Gte, Modulo }
@tokens { @tokens {
@precedence { Number Regex } @precedence { Number Regex }
@ -33,6 +33,9 @@
@precedence { @precedence {
pipe @left, pipe @left,
or @left,
and @left,
comparison @left,
multiplicative @left, multiplicative @left,
additive @left, additive @left,
call call
@ -52,6 +55,7 @@ consumeToTerminator {
FunctionDef | FunctionDef |
Assign | Assign |
BinOp | BinOp |
ConditionalOp |
expressionWithoutIdentifier expressionWithoutIdentifier
} }
@ -129,14 +133,14 @@ SingleLineThenBlock {
} }
ConditionalOp { ConditionalOp {
expression Eq expression | expression !comparison EqEq expression |
expression Neq expression | expression !comparison Neq expression |
expression Lt expression | expression !comparison Lt expression |
expression Lte expression | expression !comparison Lte expression |
expression Gt expression | expression !comparison Gt expression |
expression Gte expression | expression !comparison Gte expression |
expression And (expression | ConditionalOp) | (expression | ConditionalOp) !and And (expression | ConditionalOp) |
expression Or (expression | ConditionalOp) (expression | ConditionalOp) !or Or (expression | ConditionalOp)
} }
Params { Params {
@ -148,6 +152,7 @@ Assign {
} }
BinOp { BinOp {
expression !multiplicative Modulo expression |
(expression | BinOp) !multiplicative Star (expression | BinOp) | (expression | BinOp) !multiplicative Star (expression | BinOp) |
(expression | BinOp) !multiplicative Slash (expression | BinOp) | (expression | BinOp) !multiplicative Slash (expression | BinOp) |
(expression | BinOp) !additive Plus (expression | BinOp) | (expression | BinOp) !additive Plus (expression | BinOp) |

View File

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

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,end:76, null:82, if:92, elseif:100, else:104} const spec_Identifier = {__proto__:null,end:80, null:86, if:96, elseif:104, else:108}
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, version: 14,
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", states: "3UQYQbOOO!ZOpO'#CsO#mQcO'#CvO$jOSO'#CxO$xQbO'#EVOOQ`'#DR'#DROOQa'#DO'#DOO%{QbO'#DWO'QQcO'#DzOOQa'#Dz'#DzO)UQcO'#DyO)}QRO'#CwO*bQcO'#DuO*yQcO'#DuO+[QbO'#CuOOQ`'#Dv'#DvO,SQbO'#DuO,bQbO'#E]OOQ`'#D]'#D]O-VQRO'#DeOOQ`'#Du'#DuO-[QQO'#DtOOQ`'#Dt'#DtOOQ`'#Df'#DfQYQbOOO-dObO,59_O-lQbO'#DPOOQa'#Dy'#DyOOQ`'#DZ'#DZOOQ`'#E['#E[OOQ`'#Dm'#DmO-vQbO,59^O.ZQbO'#CzO.cQWO'#C{OOOO'#D|'#D|OOOO'#Dg'#DgO.wOSO,59dOOQa,59d,59dOOQ`'#Di'#DiO/VQbO'#DSO/_QQO,5:qOOQ`'#Dh'#DhO/dQbO,59rO/kQQO,59jOOQa,59r,59rO/vQbO,59rO,bQbO,59cO,bQbO,59cO,bQbO,59cO,bQbO,59tO,bQbO,59tO,bQbO,59tO0QQRO,59aO0XQRO,59aO0jQRO,59aO0eQQO,59aO0uQQO,59aO0}QbO'#DnO1YQbO,59]O1kQRO,5:wO1rQRO,5:wO1}QbO,5:POOQ`,5:`,5:`OOQ`-E7d-E7dOOQa1G.y1G.yOOQ`,59k,59kOOQ`-E7k-E7kOOOO,59f,59fOOOO,59g,59gOOOO-E7e-E7eOOQa1G/O1G/OOOQ`-E7g-E7gO2XQbO1G0]OOQ`-E7f-E7fO2fQQO1G/UOOQa1G/^1G/^O2qQbO1G/^OOQO'#Dk'#DkO2fQQO1G/UOOQa1G/U1G/UOOQ`'#Dl'#DlO2qQbO1G/^OOQa1G.}1G.}O3dQcO1G.}O3nQcO1G.}O3xQcO1G.}OOQa1G/`1G/`O5[QcO1G/`O5cQcO1G/`O5jQcO1G/`OOQa1G.{1G.{O!`QbO'#CvO&SQbO'#CrOOQ`,5:Y,5:YOOQ`-E7l-E7lO5qQbO1G0cOOQ`1G/k1G/kO6OQbO7+%wO6TQbO7+%xO6eQQO7+$pOOQa7+$p7+$pO6pQbO7+$xOOQa7+$x7+$xOOQO-E7i-E7iOOQ`-E7j-E7jOOQ`'#D_'#D_O6zQbO7+%}O7PQbO7+&OOOQ`<<Ic<<IcOOQ`'#Dj'#DjO7gQQO'#DjO7lQbO'#EXO8SQbO<<IdOOQa<<H[<<H[OOQa<<Hd<<HdOOQ`<<Ii<<IiOOQ`'#D`'#D`O8XQbO<<IjOOQ`,5:U,5:UOOQ`-E7h-E7hOOQ`AN?OAN?OO,bQbO'#DaOOQ`'#Do'#DoO8dQbOAN?UO8oQQO'#DcOOQ`AN?UAN?UO8tQbOAN?UO8yQRO,59{O9QQRO,59{OOQ`-E7m-E7mOOQ`G24pG24pO9]QbOG24pO9bQQO,59}O9gQQO1G/gOOQ`LD*[LD*[O6TQbO1G/iO7PQbO7+%ROOQ`7+%T7+%TOOQ`<<Hm<<Hm",
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~", stateData: "9o~O!fOS!gOS~O_QO`cOaXObPOcSOhXOpXOqXO{XO!QaO!l^O!oRO!vUO!wVO!xfO~O!kiO~O_kOaXObPOcSOhXOpXOqXOtjOylO{XO!l^O!oRO!vUO!wVO!OjX!xjX#RjX!}jXxjX~OP!mXQ!mXR!mXS!mXT!mXU!mXW!mXX!mXY!mXZ!mX[!mX]!mX^!mX~P!`OmrO!ouO!qpO!rqO~O_vOwvP~O_kOaXObPOhXOpXOqXOtjO{XO!l^O!oRO!vUO!wVO!xyO~O!||O~P%QO_kOaXObPOcSOhXOpXOqXOtjOylO{XO!l^O!oRO!vUO!wVO~OP!nXQ!nXR!nXS!nXT!nXU!nXW!nXX!nXY!nXZ!nX[!nX]!nX^!nX!x!nX#R!nX!}!nXx!nX~P&SOP!mXQ!mXR!mXS!mXT!mXU!mXW!mXX!mXY!mXZ!mX[!mX]!mX^!mX~O!x!iX#R!iXx!iX~P(ZOT!SOU!TOW!ROX!ROY!ROZ!RO[!RO]!RO~OP!POQ!POR!QOS!QO^!OO~P)cOP!POQ!POR!QOS!QO!x!iX#R!iXx!iX~OT!SOU!TO!x!iX#R!iXx!iX~O_QOaXObPOcSOhXOpXOqXO{XO!l^O!oRO!vUO!wVO~O!O!ZO!x!iX#R!iXx!iX~O_kOaXObPOhXOpXOqXO{XO!l^O!oRO!vUO!wVO~OV!_O~O!x!`O#R!`O~O_!bOh!bO~OcSOy!cO~P,bO!Ofa!xfa#Rfa!}faxfa~P&SO_!eO!l^O~O!o!fO!q!fO!r!fO!s!fO!t!fO!u!fO~OmrO!o!hO!qpO!rqO~O_vOwvX~Ow!jO~O!|!mO~P%QOtjO!x!oO!|!qO~O!x!rO!|!mO~P,bO!}!|O~P(ZOP!POQ!POR!QOS!QO!}!|O~OT!SOU!TO!}!|O~O!O!ZO!}!|O~O_!}ObPO!l^O~O!O!ZO!xea#Rea!}eaxea~Ow#RO~P)cOT!SOU!TOw#RO~O`cO!QaO~P+[O`cO!QaO!x#UO~P+[OtjO!x!oO!|#WO~O!x!rO!|#YO~P,bO^!OORkiSki!xki#Rki!}kixki~OPkiQki~P2{OP!POQ!PO~P2{OP!POQ!PORkiSki!xki#Rki!}kixki~OW!ROX!ROY!ROZ!RO[!RO]!ROT|i!x|i#R|i!}|iw|ix|i~OU!TO~P4dOU!TO~P4vOU|i~P4dO`cO!QaO!x#_O~P+[Ox#`O~O`cO!QaO!x#aOx!{P~P+[OtjO!x!oO!|#eO~O!x!rO!|#fO~P,bOx#gO~O`cO!QaO!x#aOx!{P!U!{P!W!{P~P+[O!x#jO~O`cO!QaO!x#aOx!{X!U!{X!W!{X~P+[Ox#lO~Ox#qO!U#mO!W#pO~Ox#vO!U#mO!W#pO~Ow#xO~Ox#vO~Ow#yO~P)cOT!SOU!TOw#yO~Ox#zO~O!x#{O~O!x#|O~Ohq~",
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", goto: ".z#RPPPPPPPPPPPPPPPPPPPPP#S#c#qP$i#c%d%yP&l&lPP%y&pP'T'nPPP%yP'q(^P(eP(q(t(}P)RP(e)X)_)e)k)q)z*U*`*i*pPPPP*v*z+`PP+r-PP-vPPPPPPPP-z-z._PP.g.n.nddOh!_!j#R#U#_#c#{#|R!X^i_O^h!Z!_!j#R#U#_#c#{#|fQO^h!_!j#R#U#_#c#{#|xkQVWajoz}!O!P!Q!R!S!T!n!s!}#O#X#mR!}!ZfWO^h!_!j#R#U#_#c#{#|xXQVWajoz}!O!P!Q!R!S!T!n!s!}#O#X#mQ!epR#O!Zd[Oh!_!j#R#U#_#c#{#|Q!V^Q!t!PR!w!Q!aXOQVW^ahjoz}!O!P!Q!R!S!T!_!j!n!s!}#O#R#U#X#_#c#m#{#|TrRtYmQWo!}#OQ{VQ!lzX!o{!l!p#VddOh!_!j#R#U#_#c#{#|YlQWo!}#OQ!X^R!cjRxSd]Oh!_!j#R#U#_#c#{#|Q!W^Q!^aQ!x!TQ!z!SR#t#mZmQWo!}#OedOh!_!j#R#U#_#c#{#|R#^#RQ#i#_Q#}#{R$O#|T#n#i#oQ#r#iR#w#oQhOR!ahQtRR!gtQzVR!kzQwSR!iwW#c#U#_#{#|R#k#cQ!p{Q#V!lT#Z!p#VQ!s}Q#X!nT#[!s#XWoQW!}#OR!doS![`!YR#Q![Q#o#iR#u#oTgOhSeOhQ#S!_Q#T!jQ#]#RZ#b#U#_#c#{#|d`Oh!_!j#R#U#_#c#{#|Q!Y^R#P!ZfZO^h!_!j#R#U#_#c#{#|YlQWo!}#OQ}VQ!]aQ!cjQ!nzW!r}!n!s#XQ!t!OQ!u!PQ!v!QQ!x!RQ!y!SQ!{!TR#s#mdYOh!_!j#R#U#_#c#{#|xkQVWajoz}!O!P!Q!R!S!T!n!s!}#O#X#mR!U^TsRtsTOQW^hjo!_!j!}#O#R#U#_#c#{#|Q#d#UV#h#_#{#|ZnQWo!}#OebOh!_!j#R#U#_#c#{#|",
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", nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Dict NamedArg NamedArgPrefix FunctionDef Params colon keyword Underscore Array Null ConditionalOp PositionalArg operator IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
maxTerm: 93, maxTerm: 95,
context: trackScope, context: trackScope,
nodeProps: [ nodeProps: [
["closedBy", 37,"end"] ["closedBy", 39,"end"]
], ],
propSources: [highlighting], propSources: [highlighting],
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 10, 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~", tokenData: "AO~R|OX#{XY$jYZ%TZp#{pq$jqs#{st%ntu'Vuw#{wx'[xy'ayz'zz{#{{|(e|}#{}!O(e!O!P#{!P!Q+X!Q![)S![!]3t!]!^%T!^!}#{!}#O4_#O#P6T#P#Q6Y#Q#R#{#R#S6s#S#T#{#T#Y7^#Y#Z8l#Z#b7^#b#c<z#c#f7^#f#g=q#g#h7^#h#i>h#i#o7^#o#p#{#p#q@`#q;'S#{;'S;=`$d<%l~#{~O#{~~@yS$QUmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUmS!fYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UmS!xQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZmS!gYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!gYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O!q~~'aO!o~U'hUmS!lQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUmS!}QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWmSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZYmShQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OWmSOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oWmShQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^WmSOt#{uw#{x!P#{!P!Q+v!Q#O#{#P;'S#{;'S;=`$d<%lO#{U+{^mSOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q#{!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wU-O^mSqQOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q0o!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wQ.PXqQOY-zZ!P-z!P!Q.l!Q!}-z!}#O/Z#O#P0Y#P;'S-z;'S;=`0i<%lO-zQ.oP!P!Q.rQ.wUqQ#Z#[.r#]#^.r#a#b.r#g#h.r#i#j.r#m#n.rQ/^VOY/ZZ#O/Z#O#P/s#P#Q-z#Q;'S/Z;'S;=`0S<%lO/ZQ/vSOY/ZZ;'S/Z;'S;=`0S<%lO/ZQ0VP;=`<%l/ZQ0]SOY-zZ;'S-z;'S;=`0i<%lO-zQ0lP;=`<%l-zU0tWmSOt#{uw#{x!P#{!P!Q1^!Q#O#{#P;'S#{;'S;=`$d<%lO#{U1ebmSqQOt#{uw#{x#O#{#P#Z#{#Z#[1^#[#]#{#]#^1^#^#a#{#a#b1^#b#g#{#g#h1^#h#i#{#i#j1^#j#m#{#m#n1^#n;'S#{;'S;=`$d<%lO#{U2r[mSOY2mYZ#{Zt2mtu/Zuw2mwx/Zx#O2m#O#P/s#P#Q,w#Q;'S2m;'S;=`3h<%lO2mU3kP;=`<%l2mU3qP;=`<%l,wU3{UmSwQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4fW!wQmSOt#{uw#{x!_#{!_!`5O!`#O#{#P;'S#{;'S;=`$d<%lO#{U5TVmSOt#{uw#{x#O#{#P#Q5j#Q;'S#{;'S;=`$d<%lO#{U5qU!vQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6YO!r~U6aU!|QmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6zUmSyQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7cYmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{U8YUtQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U8qZmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#U9d#U#o7^#o;'S#{;'S;=`$d<%lO#{U9i[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#`7^#`#a:_#a#o7^#o;'S#{;'S;=`$d<%lO#{U:d[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#g7^#g#h;Y#h#o7^#o;'S#{;'S;=`$d<%lO#{U;_[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#X7^#X#Y<T#Y#o7^#o;'S#{;'S;=`$d<%lO#{U<[YpQmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=RY!sWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=xY!uWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^>o[!tWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#f7^#f#g?e#g#o7^#o;'S#{;'S;=`$d<%lO#{U?j[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#i7^#i#j;Y#j#o7^#o;'S#{;'S;=`$d<%lO#{U@gU!OQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~AOO#R~",
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!i~~", 11)], tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!k~~", 11)],
topRules: {"Program":[0,18]}, topRules: {"Program":[0,20]},
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: 15, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 15, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
tokenPrec: 1008 tokenPrec: 1132
}) })

View File

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

View File

@ -4,12 +4,12 @@ import '../shrimp.grammar' // Importing this so changes cause it to retest!
describe('if/elseif/else', () => { describe('if/elseif/else', () => {
test('parses single line if', () => { test('parses single line if', () => {
expect(`if y = 1: 'cool' end`).toMatchTree(` expect(`if y == 1: 'cool' end`).toMatchTree(`
IfExpr IfExpr
keyword if keyword if
ConditionalOp ConditionalOp
Identifier y Identifier y
Eq = EqEq ==
Number 1 Number 1
colon : colon :
SingleLineThenBlock SingleLineThenBlock

View File

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

View File

@ -43,18 +43,144 @@ export const globalFunctions = {
} }
}, },
// type predicates
'string?': (v: any) => toValue(v).type === 'string',
'number?': (v: any) => toValue(v).type === 'number',
'boolean?': (v: any) => toValue(v).type === 'boolean',
'array?': (v: any) => toValue(v).type === 'array',
'dict?': (v: any) => toValue(v).type === 'dict',
'function?': (v: any) => {
const t = toValue(v).type
return t === 'function' || t === 'native'
},
'null?': (v: any) => toValue(v).type === 'null',
'some?': (v: any) => toValue(v).type !== 'null',
// boolean/logic
not: (v: any) => !v,
// utilities
inc: (n: number) => n + 1,
dec: (n: number) => n - 1,
identity: (v: any) => v,
// strings // strings
join: (arr: string[], sep: string = ',') => arr.join(sep), str: {
split: (str: string, sep: string = ',') => str.split(sep), join: (arr: string[], sep: string = ',') => arr.join(sep),
'to-upper': (str: string) => str.toUpperCase(), split: (str: string, sep: string = ',') => str.split(sep),
'to-lower': (str: string) => str.toLowerCase(), 'to-upper': (str: string) => str.toUpperCase(),
trim: (str: string) => str.trim(), 'to-lower': (str: string) => str.toLowerCase(),
trim: (str: string) => str.trim(),
// predicates
'starts-with?': (str: string, prefix: string) => str.startsWith(prefix),
'ends-with?': (str: string, suffix: string) => str.endsWith(suffix),
'contains?': (str: string, substr: string) => str.includes(substr),
'empty?': (str: string) => str.length === 0,
// transformations
replace: (str: string, search: string, replacement: string) => str.replace(search, replacement),
'replace-all': (str: string, search: string, replacement: string) => str.replaceAll(search, replacement),
slice: (str: string, start: number, end?: number | null) => str.slice(start, end ?? undefined),
substring: (str: string, start: number, end?: number | null) => str.substring(start, end ?? undefined),
repeat: (str: string, count: number) => str.repeat(count),
'pad-start': (str: string, length: number, pad: string = ' ') => str.padStart(length, pad),
'pad-end': (str: string, length: number, pad: string = ' ') => str.padEnd(length, pad),
lines: (str: string) => str.split('\n'),
chars: (str: string) => str.split(''),
'index-of': (str: string, search: string) => str.indexOf(search),
'last-index-of': (str: string, search: string) => str.lastIndexOf(search),
match: (str: string, regex: RegExp) => str.match(regex),
'test?': (str: string, regex: RegExp) => regex.test(str),
},
// list
list: {
slice: (list: any[], start: number, end?: number) => list.slice(start, end),
map: async (list: any[], cb: Function) => {
let acc: any[] = []
for (const value of list) acc.push(await cb(value))
return acc
},
filter: async (list: any[], cb: Function) => {
let acc: any[] = []
for (const value of list) {
if (await cb(value)) acc.push(value)
}
return acc
},
reduce: async (list: any[], cb: Function, initial: any) => {
let acc = initial
for (const value of list) acc = await cb(acc, value)
return acc
},
find: async (list: any[], cb: Function) => {
for (const value of list) {
if (await cb(value)) return value
}
return null
},
// predicates
'empty?': (list: any[]) => list.length === 0,
'contains?': (list: any[], item: any) => list.includes(item),
'any?': async (list: any[], cb: Function) => {
for (const value of list) {
if (await cb(value)) return true
}
return false
},
'all?': async (list: any[], cb: Function) => {
for (const value of list) {
if (!await cb(value)) return false
}
return true
},
// sequence operations
reverse: (list: any[]) => list.slice().reverse(),
sort: (list: any[], cb?: (a: any, b: any) => number) => list.slice().sort(cb),
concat: (...lists: any[][]) => lists.flat(1),
flatten: (list: any[], depth: number = 1) => list.flat(depth),
unique: (list: any[]) => Array.from(new Set(list)),
zip: (list1: any[], list2: any[]) => list1.map((item, i) => [item, list2[i]]),
// access
first: (list: any[]) => list[0] ?? null,
last: (list: any[]) => list[list.length - 1] ?? null,
rest: (list: any[]) => list.slice(1),
take: (list: any[], n: number) => list.slice(0, n),
drop: (list: any[], n: number) => list.slice(n),
append: (list: any[], item: any) => [...list, item],
prepend: (list: any[], item: any) => [item, ...list],
'index-of': (list: any[], item: any) => list.indexOf(item),
// utilities
sum: (list: any[]) => list.reduce((acc, x) => acc + x, 0),
count: async (list: any[], cb: Function) => {
let count = 0
for (const value of list) {
if (await cb(value)) count++
}
return count
},
partition: async (list: any[], cb: Function) => {
const truthy: any[] = []
const falsy: any[] = []
for (const value of list) {
if (await cb(value)) truthy.push(value)
else falsy.push(value)
}
return [truthy, falsy]
},
compact: (list: any[]) => list.filter(x => x != null),
'group-by': async (list: any[], cb: Function) => {
const groups: Record<string, any[]> = {}
for (const value of list) {
const key = String(await cb(value))
if (!groups[key]) groups[key] = []
groups[key].push(value)
}
return groups
},
},
// collections // collections
at: (collection: any, index: number | string) => collection[index], at: (collection: any, index: number | string) => collection[index],
list: (...args: any[]) => args,
dict: (atNamed = {}) => atNamed,
slice: (list: any[], start: number, end?: number) => list.slice(start, end),
range: (start: number, end: number | null) => { range: (start: number, end: number | null) => {
if (end === null) { if (end === null) {
end = start end = start
@ -66,13 +192,67 @@ export const globalFunctions = {
} }
return result return result
}, },
'empty?': (v: any) => {
const value = toValue(v)
switch (value.type) {
case 'string': case 'array':
return value.value.length === 0
case 'dict':
return value.value.size === 0
default:
return false
}
},
// dict
dict: {
keys: (dict: Record<string, any>) => Object.keys(dict),
values: (dict: Record<string, any>) => Object.values(dict),
entries: (dict: Record<string, any>) => Object.entries(dict).map(([k, v]) => ({ key: k, value: v })),
'has?': (dict: Record<string, any>, key: string) => key in dict,
get: (dict: Record<string, any>, key: string, defaultValue: any = null) => dict[key] ?? defaultValue,
merge: (...dicts: Record<string, any>[]) => Object.assign({}, ...dicts),
'empty?': (dict: Record<string, any>) => Object.keys(dict).length === 0,
map: async (dict: Record<string, any>, cb: Function) => {
const result: Record<string, any> = {}
for (const [key, value] of Object.entries(dict)) {
result[key] = await cb(value, key)
}
return result
},
filter: async (dict: Record<string, any>, cb: Function) => {
const result: Record<string, any> = {}
for (const [key, value] of Object.entries(dict)) {
if (await cb(value, key)) result[key] = value
}
return result
},
'from-entries': (entries: [string, any][]) => Object.fromEntries(entries),
},
// math
math: {
abs: (n: number) => Math.abs(n),
floor: (n: number) => Math.floor(n),
ceil: (n: number) => Math.ceil(n),
round: (n: number) => Math.round(n),
min: (...nums: number[]) => Math.min(...nums),
max: (...nums: number[]) => Math.max(...nums),
pow: (base: number, exp: number) => Math.pow(base, exp),
sqrt: (n: number) => Math.sqrt(n),
random: () => Math.random(),
clamp: (n: number, min: number, max: number) => Math.min(Math.max(n, min), max),
sign: (n: number) => Math.sign(n),
trunc: (n: number) => Math.trunc(n),
// predicates
'even?': (n: number) => n % 2 === 0,
'odd?': (n: number) => n % 2 !== 0,
'positive?': (n: number) => n > 0,
'negative?': (n: number) => n < 0,
'zero?': (n: number) => n === 0,
},
// enumerables // enumerables
map: async (list: any[], cb: Function) => {
let acc: any[] = []
for (const value of list) acc.push(await cb(value))
return acc
},
each: async (list: any[], cb: Function) => { each: async (list: any[], cb: Function) => {
for (const value of list) await cb(value) for (const value of list) await cb(value)
return list return list

View File

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

View File

@ -1,9 +1,10 @@
import { expect } from 'bun:test' import { expect } from 'bun:test'
import { parser } from '#parser/shrimp' import { parser } from '#parser/shrimp'
import { setGlobals } from '#parser/tokenizer'
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, type TypeScriptFunction } from 'reefvm' import { run, VM } from 'reefvm'
import { treeToString, VMResultToValue } from '#utils/tree' import { treeToString, VMResultToValue } from '#utils/tree'
const regenerateParser = async () => { const regenerateParser = async () => {
@ -30,7 +31,7 @@ await regenerateParser()
// Type declaration for TypeScript // Type declaration for TypeScript
declare module 'bun:test' { declare module 'bun:test' {
interface Matchers<T> { interface Matchers<T> {
toMatchTree(expected: string): T toMatchTree(expected: string, globals?: Record<string, any>): T
toMatchExpression(expected: string): T toMatchExpression(expected: string): T
toFailParse(): T toFailParse(): T
toEvaluateTo(expected: unknown, globals?: Record<string, any>): Promise<T> toEvaluateTo(expected: unknown, globals?: Record<string, any>): Promise<T>
@ -39,9 +40,10 @@ declare module 'bun:test' {
} }
expect.extend({ expect.extend({
toMatchTree(received: unknown, expected: string) { toMatchTree(received: unknown, expected: string, globals?: Record<string, any>) {
assert(typeof received === 'string', 'toMatchTree can only be used with string values') assert(typeof received === 'string', 'toMatchTree can only be used with string values')
if (globals) setGlobals(Object.keys(globals))
const tree = parser.parse(received) const tree = parser.parse(received)
const actual = treeToString(tree, received) const actual = treeToString(tree, received)
const normalizedExpected = trimWhitespace(expected) const normalizedExpected = trimWhitespace(expected)
@ -97,6 +99,7 @@ expect.extend({
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 {
if (globals) setGlobals(Object.keys(globals))
const compiler = new Compiler(received) const compiler = new Compiler(received)
const result = await run(compiler.bytecode, globals) const result = await run(compiler.bytecode, globals)
let value = VMResultToValue(result) let value = VMResultToValue(result)