diff --git a/bun.lock b/bun.lock index 559b57e..f00ae05 100644 --- a/bun.lock +++ b/bun.lock @@ -2,7 +2,7 @@ "lockfileVersion": 1, "workspaces": { "": { - "name": "bun-react-template", + "name": "shrimp", "dependencies": { "@codemirror/view": "^6.38.3", "@lezer/generator": "^1.8.0", @@ -20,39 +20,39 @@ }, }, "packages": { - "@codemirror/autocomplete": ["@codemirror/autocomplete@6.19.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg=="], + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.19.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw=="], - "@codemirror/commands": ["@codemirror/commands@6.8.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw=="], + "@codemirror/commands": ["@codemirror/commands@6.10.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w=="], "@codemirror/language": ["@codemirror/language@6.11.3", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA=="], - "@codemirror/lint": ["@codemirror/lint@6.8.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA=="], + "@codemirror/lint": ["@codemirror/lint@6.9.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ=="], "@codemirror/search": ["@codemirror/search@6.5.11", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "crelt": "^1.0.5" } }, "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA=="], "@codemirror/state": ["@codemirror/state@6.5.2", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA=="], - "@codemirror/view": ["@codemirror/view@6.38.3", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-x2t87+oqwB1mduiQZ6huIghjMt4uZKFEdj66IcXw7+a5iBEvv9lh7EWDRHI7crnD4BMGpnyq/RzmCGbiEZLcvQ=="], + "@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="], - "@lezer/common": ["@lezer/common@1.2.3", "", {}, "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="], + "@lezer/common": ["@lezer/common@1.3.0", "", {}, "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ=="], "@lezer/generator": ["@lezer/generator@1.8.0", "", { "dependencies": { "@lezer/common": "^1.1.0", "@lezer/lr": "^1.3.0" }, "bin": { "lezer-generator": "src/lezer-generator.cjs" } }, "sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg=="], - "@lezer/highlight": ["@lezer/highlight@1.2.1", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA=="], + "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="], - "@lezer/lr": ["@lezer/lr@1.4.2", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA=="], + "@lezer/lr": ["@lezer/lr@1.4.3", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA=="], "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], - "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], + "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], - "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], + "@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], - "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], "bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.15", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-qtAXMNGG4R0UGGI8zWrqm2B7BdXqx48vunJXBPzfDOHPA5WkRUZdTSbE7TFwO4jLhYqSE23YMWsM9NhE6ovobw=="], - "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], "codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="], @@ -60,17 +60,17 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="], + "hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="], - "reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#0f39e9401eb7a0a7c906e150127f9829458a79b6", { "peerDependencies": { "typescript": "^5" } }, "0f39e9401eb7a0a7c906e150127f9829458a79b6"], + "reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#33ea94a2476f87f22408dce8b7beb3587070e01b", { "peerDependencies": { "typescript": "^5" } }, "33ea94a2476f87f22408dce8b7beb3587070e01b"], - "style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="], + "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], - "tailwindcss": ["tailwindcss@4.1.13", "", {}, "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="], + "tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="], - "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], } diff --git a/package.json b/package.json index b728a9a..f3fd090 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dev": "bun generate-parser && bun --hot src/server/server.tsx", "generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts", "repl": "bun generate-parser && bun bin/repl", - "update-reef": "rm -rf ~/.bun/install/cache/ && bun update reefvm" + "update-reef": "rm -rf ~/.bun/install/cache/ && rm bun.lock && bun update reefvm" }, "dependencies": { "@codemirror/view": "^6.38.3", @@ -29,4 +29,4 @@ "singleQuote": true, "printWidth": 100 } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 48fa3e7..3a91bed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,40 @@ export { Compiler } from '#compiler/compiler' export { parser } from '#parser/shrimp' export { globals } from '#prelude' +export class Shrimp { + vm: VM + private globals?: Record + + constructor(globals?: Record) { + const emptyBytecode = { instructions: [], constants: [], labels: new Map() } + this.vm = new VM(emptyBytecode, Object.assign({}, shrimpGlobals, globals ?? {})) + this.globals = globals + } + + async run(code: string | Bytecode, locals?: Record): Promise { + let bytecode + + if (typeof code === 'string') { + const compiler = new Compiler(code, Object.keys(Object.assign({}, shrimpGlobals, this.globals ?? {}, locals ?? {}))) + bytecode = compiler.bytecode + } else { + bytecode = code + } + + if (locals) this.vm.pushScope(locals) + this.vm.appendBytecode(bytecode) + await this.vm.continue() + if (locals) this.vm.popScope() + + return this.vm.stack.length ? fromValue(this.vm.stack.at(-1)!) : null + } + + get(name: string): any { + const value = this.vm.scope.get(name) + return value ? fromValue(value) : null + } +} + export async function runFile(path: string, globals?: Record): Promise { const code = readFileSync(path, 'utf-8') return await runCode(code, globals) diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 1cad017..e9bc2ec 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -120,12 +120,16 @@ FunctionDef { Do Params colon (consumeToTerminator | newlineOrSemicolon block) CatchExpr? FinallyExpr? end } +ifTest { + ConditionalOp | expression | FunctionCall +} + IfExpr { - if (ConditionalOp | expression) colon Block ElseIfExpr* ElseExpr? end + if ifTest colon Block ElseIfExpr* ElseExpr? end } ElseIfExpr { - else if (ConditionalOp | expression) colon Block + else if ifTest colon Block } ElseExpr { @@ -184,7 +188,7 @@ BinOp { } ParenExpr { - leftParen (ambiguousFunctionCall | BinOp | expressionWithoutIdentifier | ConditionalOp | PipeExpr | FunctionDef) rightParen + leftParen (IfExpr | ambiguousFunctionCall | BinOp | expressionWithoutIdentifier | ConditionalOp | PipeExpr | FunctionDef) rightParen } expression { diff --git a/src/parser/tests/control-flow.test.ts b/src/parser/tests/control-flow.test.ts index af0f704..1bacc31 100644 --- a/src/parser/tests/control-flow.test.ts +++ b/src/parser/tests/control-flow.test.ts @@ -156,6 +156,103 @@ describe('if/else if/else', () => { keyword end `) }) + + test('parses function calls in if tests', () => { + expect(`if var? 'abc': true end`).toMatchTree(` + IfExpr + keyword if + FunctionCall + Identifier var? + PositionalArg + String + StringFragment abc + colon : + Block + Boolean true + keyword end + `) + }) + + test('parses function calls in if tests', () => { + expect(`if (var? 'abc'): true end`).toMatchTree(` + IfExpr + keyword if + ParenExpr + FunctionCall + Identifier var? + PositionalArg + String + StringFragment abc + colon : + Block + Boolean true + keyword end + `) + }) + + + test('parses function calls in else-if tests', () => { + expect(`if false: true else if var? 'abc': true end`).toMatchTree(` + IfExpr + keyword if + Boolean false + colon : + Block + Boolean true + ElseIfExpr + keyword else + keyword if + FunctionCall + Identifier var? + PositionalArg + String + StringFragment abc + colon : + Block + Boolean true + keyword end + `) + }) + + test('parses function calls in else-if tests', () => { + expect(`if false: true else if (var? 'abc'): true end`).toMatchTree(` + IfExpr + keyword if + Boolean false + colon : + Block + Boolean true + ElseIfExpr + keyword else + keyword if + ParenExpr + FunctionCall + Identifier var? + PositionalArg + String + StringFragment abc + colon : + Block + Boolean true + keyword end + `) + }) + + test('allows if/else in parens', () => { + expect(`eh? = (if true: true end)`).toMatchTree(` + Assign + AssignableIdentifier eh? + Eq = + ParenExpr + IfExpr + keyword if + Boolean true + colon : + Block + Boolean true + keyword end + `) + }) }) describe('while', () => { diff --git a/src/prelude/index.ts b/src/prelude/index.ts index cc46ead..488414b 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -1,7 +1,7 @@ // The prelude creates all the builtin Shrimp functions. import { - type Value, toValue, + type Value, type VM, toValue, extractParamInfo, isWrapped, getOriginalFunction, } from 'reefvm' @@ -34,13 +34,11 @@ export const globals = { const val = toValue(v) return `#<${val.type}: ${formatValue(val)}>` }, - length: (v: any) => { - const value = toValue(v) - switch (value.type) { - case 'string': case 'array': return value.value.length - case 'dict': return value.value.size - default: throw new Error(`length: expected string, array, or dict, got ${value.type}`) - } + var: function (this: VM, v: any) { + return typeof v === 'string' ? this.scope.get(v) : v + }, + 'var?': function (this: VM, v: string) { + return typeof v !== 'string' || this.scope.has(v) }, // type predicates @@ -65,6 +63,14 @@ export const globals = { identity: (v: any) => v, // collections + length: (v: any) => { + const value = toValue(v) + switch (value.type) { + case 'string': case 'array': return value.value.length + case 'dict': return value.value.size + default: throw new Error(`length: expected string, array, or dict, got ${value.type}`) + } + }, at: (collection: any, index: number | string) => { const value = toValue(collection) if (value.type === 'string' || value.type === 'array') { diff --git a/src/prelude/tests/info.test.ts b/src/prelude/tests/info.test.ts new file mode 100644 index 0000000..9c24a8a --- /dev/null +++ b/src/prelude/tests/info.test.ts @@ -0,0 +1,79 @@ +import { expect, describe, test } from 'bun:test' +import { globals } from '#prelude' + +describe('var and var?', () => { + test('var? checks if a variable exists', async () => { + await expect(`var? 'nada'`).toEvaluateTo(false, globals) + await expect(`var? 'info'`).toEvaluateTo(false, globals) + await expect(`abc = abc; var? 'abc'`).toEvaluateTo(true, globals) + await expect(`var? 'var?'`).toEvaluateTo(true, globals) + + await expect(`var? 'dict'`).toEvaluateTo(true, globals) + await expect(`var? dict`).toEvaluateTo(true, globals) + }) + + test('var returns a value or null', async () => { + await expect(`var 'nada'`).toEvaluateTo(null, globals) + await expect(`var nada`).toEvaluateTo(null, globals) + await expect(`var 'info'`).toEvaluateTo(null, globals) + await expect(`abc = my-string; var 'abc'`).toEvaluateTo('my-string', globals) + await expect(`abc = my-string; var abc`).toEvaluateTo(null, globals) + }) +}) + +describe('type predicates', () => { + test('string? checks for string type', async () => { + await expect(`string? 'hello'`).toEvaluateTo(true, globals) + await expect(`string? 42`).toEvaluateTo(false, globals) + }) + + test('number? checks for number type', async () => { + await expect(`number? 42`).toEvaluateTo(true, globals) + await expect(`number? 'hello'`).toEvaluateTo(false, globals) + }) + + test('boolean? checks for boolean type', async () => { + await expect(`boolean? true`).toEvaluateTo(true, globals) + await expect(`boolean? 42`).toEvaluateTo(false, globals) + }) + + test('array? checks for array type', async () => { + await expect(`array? [1 2 3]`).toEvaluateTo(true, globals) + await expect(`array? 42`).toEvaluateTo(false, globals) + }) + + test('dict? checks for dict type', async () => { + await expect(`dict? [a=1]`).toEvaluateTo(true, globals) + await expect(`dict? []`).toEvaluateTo(false, globals) + }) + + test('null? checks for null type', async () => { + await expect(`null? null`).toEvaluateTo(true, globals) + await expect(`null? 42`).toEvaluateTo(false, globals) + }) + + test('some? checks for non-null', async () => { + await expect(`some? 42`).toEvaluateTo(true, globals) + await expect(`some? null`).toEvaluateTo(false, globals) + }) +}) + +describe('introspection', () => { + test('type returns proper types', async () => { + await expect(`type 'hello'`).toEvaluateTo('string', globals) + await expect(`type 42`).toEvaluateTo('number', globals) + await expect(`type true`).toEvaluateTo('boolean', globals) + await expect(`type false`).toEvaluateTo('boolean', globals) + await expect(`type null`).toEvaluateTo('null', globals) + await expect(`type [1 2 3]`).toEvaluateTo('array', globals) + await expect(`type [a=1 b=2]`).toEvaluateTo('dict', globals) + }) + + test('inspect formats values', async () => { + await expect(`inspect 'hello'`).toEvaluateTo("\u001b[32m'hello\u001b[32m'\u001b[0m", globals) + }) + + test('describe describes values', async () => { + await expect(`describe 'hello'`).toEvaluateTo("#", globals) + }) +}) diff --git a/src/prelude/tests/prelude.test.ts b/src/prelude/tests/prelude.test.ts index 40b7809..3125f9b 100644 --- a/src/prelude/tests/prelude.test.ts +++ b/src/prelude/tests/prelude.test.ts @@ -98,43 +98,6 @@ describe('string operations', () => { }) }) -describe('type predicates', () => { - test('string? checks for string type', async () => { - await expect(`string? 'hello'`).toEvaluateTo(true, globals) - await expect(`string? 42`).toEvaluateTo(false, globals) - }) - - test('number? checks for number type', async () => { - await expect(`number? 42`).toEvaluateTo(true, globals) - await expect(`number? 'hello'`).toEvaluateTo(false, globals) - }) - - test('boolean? checks for boolean type', async () => { - await expect(`boolean? true`).toEvaluateTo(true, globals) - await expect(`boolean? 42`).toEvaluateTo(false, globals) - }) - - test('array? checks for array type', async () => { - await expect(`array? [1 2 3]`).toEvaluateTo(true, globals) - await expect(`array? 42`).toEvaluateTo(false, globals) - }) - - test('dict? checks for dict type', async () => { - await expect(`dict? [a=1]`).toEvaluateTo(true, globals) - await expect(`dict? []`).toEvaluateTo(false, globals) - }) - - test('null? checks for null type', async () => { - await expect(`null? null`).toEvaluateTo(true, globals) - await expect(`null? 42`).toEvaluateTo(false, globals) - }) - - test('some? checks for non-null', async () => { - await expect(`some? 42`).toEvaluateTo(true, globals) - await expect(`some? null`).toEvaluateTo(false, globals) - }) -}) - describe('boolean logic', () => { test('not negates value', async () => { await expect(`not true`).toEvaluateTo(false, globals) @@ -161,17 +124,7 @@ describe('utilities', () => { }) }) -describe('introspection', () => { - test('type returns proper types', async () => { - await expect(`type 'hello'`).toEvaluateTo('string', globals) - await expect(`type 42`).toEvaluateTo('number', globals) - await expect(`type true`).toEvaluateTo('boolean', globals) - await expect(`type false`).toEvaluateTo('boolean', globals) - await expect(`type null`).toEvaluateTo('null', globals) - await expect(`type [1 2 3]`).toEvaluateTo('array', globals) - await expect(`type [a=1 b=2]`).toEvaluateTo('dict', globals) - }) - +describe('collections', () => { test('length', async () => { await expect(`length 'hello'`).toEvaluateTo(5, globals) await expect(`length [1 2 3]`).toEvaluateTo(3, globals) @@ -184,20 +137,6 @@ describe('introspection', () => { await expect(`try: length null catch e: 'error' end`).toEvaluateTo('error', globals) }) - test('inspect formats values', async () => { - // Just test that inspect returns something for now - // (we'd need more complex assertion to check the actual format) - await expect(`type (inspect 'hello')`).toEvaluateTo('string', globals) - }) - - test('describe describes values', async () => { - // Just test that inspect returns something for now - // (we'd need more complex assertion to check the actual format) - await expect(`describe 'hello'`).toEvaluateTo("#", globals) - }) -}) - -describe('collections', () => { test('literal array creates array from arguments', async () => { await expect(`[ 1 2 3 ]`).toEvaluateTo([1, 2, 3], globals) await expect(`['a' 'b']`).toEvaluateTo(['a', 'b'], globals) diff --git a/src/tests/shrimp.test.ts b/src/tests/shrimp.test.ts new file mode 100644 index 0000000..3f7a6f8 --- /dev/null +++ b/src/tests/shrimp.test.ts @@ -0,0 +1,53 @@ +import { describe } from 'bun:test' +import { expect, test } from 'bun:test' +import { Shrimp } from '..' + +describe('Shrimp', () => { + test('allows running Shrimp code', async () => { + const shrimp = new Shrimp() + expect(await shrimp.run(`1 + 5`)).toEqual(6) + expect(await shrimp.run(`type 5`)).toEqual('number') + }) + + test('maintains state across runs', async () => { + const shrimp = new Shrimp() + + await shrimp.run(`abc = true`) + expect(shrimp.get('abc')).toEqual(true) + + await shrimp.run(`name = Bob`) + expect(shrimp.get('abc')).toEqual(true) + expect(shrimp.get('name')).toEqual('Bob') + + await shrimp.run(`abc = false`) + expect(shrimp.get('abc')).toEqual(false) + }) + + test('allows setting your own globals', async () => { + const shrimp = new Shrimp({ hiya: () => 'hey there' }) + + await shrimp.run('abc = hiya') + expect(shrimp.get('abc')).toEqual('hey there') + expect(await shrimp.run('type abc')).toEqual('string') + + // still there + expect(await shrimp.run('hiya')).toEqual('hey there') + }) + + test('allows setting your own locals', async () => { + const shrimp = new Shrimp({ 'my-global': () => 'hey there' }) + + await shrimp.run('abc = my-global') + expect(shrimp.get('abc')).toEqual('hey there') + + await shrimp.run('abc = my-global', { 'my-global': 'now a local' }) + expect(shrimp.get('abc')).toEqual('now a local') + + await shrimp.run('abc = nothing') + expect(shrimp.get('abc')).toEqual('nothing') + await shrimp.run('abc = nothing', { nothing: 'something' }) + expect(shrimp.get('abc')).toEqual('something') + await shrimp.run('abc = nothing') + expect(shrimp.get('abc')).toEqual('nothing') + }) +}) \ No newline at end of file