diff --git a/bin/shrimp b/bin/shrimp index d26ec68..fe856b3 100755 --- a/bin/shrimp +++ b/bin/shrimp @@ -2,34 +2,78 @@ import { colors } from '../src/prelude' import { treeToString } from '../src/utils/tree' -import { runFile, compileFile, parseCode } from '../src' +import { runCode, runFile, compileFile, parseCode } from '../src' import { bytecodeToString } from 'reefvm' -import { readFileSync, writeFileSync, mkdirSync } from 'fs' -import { randomUUID } from 'crypto' +import { readFileSync } from 'fs' import { spawn } from 'child_process' import { join } from 'path' function showHelp() { console.log(`${colors.bright}${colors.magenta}🦐 Shrimp${colors.reset} is a scripting language in a shell. -${colors.bright}Usage:${colors.reset} shrimp [...args] +${colors.bright}Usage:${colors.reset} shrimp [options] [...args] ${colors.bright}Commands:${colors.reset} ${colors.cyan}run ${colors.yellow}./my-file.sh${colors.reset} Execute a file with Shrimp ${colors.cyan}parse ${colors.yellow}./my-file.sh${colors.reset} Print parse tree for Shrimp file ${colors.cyan}bytecode ${colors.yellow}./my-file.sh${colors.reset} Print bytecode for Shrimp file - ${colors.cyan}eval ${colors.yellow}'some code'${colors.reset} Evaluate a line of Shrimp code + ${colors.cyan}eval ${colors.yellow}'some code'${colors.reset} Evaluate a line of Shrimp code + ${colors.cyan}print ${colors.yellow}'some code'${colors.reset} Evaluate a line of Shrimp code and print the result ${colors.cyan}repl${colors.reset} Start REPL ${colors.cyan}help${colors.reset} Print this help message - ${colors.cyan}version${colors.reset} Print version`) + ${colors.cyan}version${colors.reset} Print version + +${colors.bright}Options:${colors.reset} + ${colors.cyan}eval -I${colors.reset} ${colors.yellow}${colors.reset} Import module (can be repeated) + Example: shrimp -I math -e 'random | echo' + Example: shrimp -Imath -Istr -e 'random | echo'`) } function showVersion() { console.log('🦐 v0.0.1') } +async function evalCode(code: string, imports: string[]) { + const importStatement = imports.length > 0 ? `import ${imports.join(' ')}` : '' + if (importStatement) code = `${importStatement}; ${code}` + return await runCode(code) +} + async function main() { - const args = process.argv.slice(2) + let args = process.argv.slice(2) + + if (args.length === 0) { + showHelp() + return + } + + // Parse -I flags for imports (supports both "-I math" and "-Imath") + const imports: string[] = [] + + while (args.length > 0) { + const arg = args[0] + + if (arg === '-I') { + // "-I math" format + if (args.length < 2) { + console.log(`${colors.bright}error: -I requires a module name${colors.reset}`) + process.exit(1) + } + imports.push(args[1]) + args = args.slice(2) + } else if (arg.startsWith('-I')) { + // "-Imath" format + const moduleName = arg.slice(2) + if (!moduleName) { + console.log(`${colors.bright}error: -I requires a module name${colors.reset}`) + process.exit(1) + } + imports.push(moduleName) + args = args.slice(1) + } else { + break + } + } if (args.length === 0) { showHelp() @@ -63,10 +107,18 @@ async function main() { process.exit(1) } - try { mkdirSync('/tmp/shrimp') } catch { } - const path = `/tmp/shrimp/${randomUUID()}.sh` - writeFileSync(path, code) - console.log(await runFile(path)) + await evalCode(code, imports) + return + } + + if (['print', '-print', '--print', '-E'].includes(command)) { + const code = args[1] + if (!code) { + console.log(`${colors.bright}usage: shrimp print ${colors.reset}`) + process.exit(1) + } + + console.log(await evalCode(code, imports)) return } diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 80ce825..d52576e 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -797,6 +797,31 @@ export class Compiler { return instructions } + case terms.Import: { + const instructions: ProgramItem[] = [] + const [_import, ...nodes] = getAllChildren(node) + const args = nodes.filter(node => node.type.id === terms.Identifier) + const namedArgs = nodes.filter(node => node.type.id === terms.NamedArg) + + instructions.push(['LOAD', 'import']) + + args.forEach((dict) => + instructions.push(['PUSH', input.slice(dict.from, dict.to)]) + ) + + namedArgs.forEach((arg) => { + const { name, valueNode } = getNamedArgParts(arg, input) + instructions.push(['PUSH', name]) + instructions.push(...this.#compileNode(valueNode, input)) + }) + + instructions.push(['PUSH', args.length]) + instructions.push(['PUSH', namedArgs.length]) + instructions.push(['CALL']) + + return instructions + } + case terms.Comment: { return [] // ignore comments } diff --git a/src/compiler/tests/compiler.test.ts b/src/compiler/tests/compiler.test.ts index 1965e89..45fdb39 100644 --- a/src/compiler/tests/compiler.test.ts +++ b/src/compiler/tests/compiler.test.ts @@ -448,3 +448,31 @@ describe('Compound assignment operators', () => { expect('x = 10; x %= 3; x').toEvaluateTo(1) }) }) + +describe('import', () => { + test('imports single dict', () => { + expect(`import str; starts-with? abc a`).toEvaluateTo(true) + }) + + test('imports multiple dicts', () => { + expect(`import str math list; map [1 2 3] do x: x * 2 end`).toEvaluateTo([2, 4, 6]) + }) + + test('imports non-prelude dicts', () => { + expect(` + abc = [a=true b=yes c=si] + import abc + abc.b + `).toEvaluateTo('yes') + }) + + test('can specify imports', () => { + expect(`import str only=ends-with?; ref ends-with? | function?`).toEvaluateTo(true) + expect(`import str only=ends-with?; ref starts-with? | function?`).toEvaluateTo(false) + expect(` + abc = [a=true b=yes c=si] + import abc only=[a c] + [a c] + `).toEvaluateTo([true, 'si']) + }) +}) \ No newline at end of file diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 506f036..80adcdb 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -41,6 +41,7 @@ try { @specialize[@name=keyword] } catch { @specialize[@name=keyword] } finally { @specialize[@name=keyword] } throw { @specialize[@name=keyword] } +import { @specialize[@name=keyword] } null { @specialize[@name=Null] } @external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot, CurlyString } @@ -72,6 +73,7 @@ consumeToTerminator { ambiguousFunctionCall | TryExpr | Throw | + Import | IfExpr | FunctionDef | CompoundAssign | @@ -161,6 +163,11 @@ Throw { throw (BinOp | ConditionalOp | expression) } +// this has to be in the parse tree so the scope tracker can use it +Import { + import NamedArg* Identifier+ NamedArg* +} + ConditionalOp { expression !comparison EqEq expression | expression !comparison Neq expression | diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index 0f49afe..0f3ba5b 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -37,7 +37,7 @@ export const Program = 35, PipeExpr = 36, WhileExpr = 38, - keyword = 81, + keyword = 83, ConditionalOp = 40, ParenExpr = 41, FunctionCallWithNewlines = 42, @@ -73,5 +73,6 @@ export const FunctionCallWithBlock = 77, TryExpr = 78, Throw = 80, - CompoundAssign = 82, - Assign = 83 + Import = 82, + CompoundAssign = 84, + Assign = 85 diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 6435fd6..051f00e 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -4,24 +4,24 @@ import {operatorTokenizer} from "./operatorTokenizer" import {tokenizer, specializeKeyword} from "./tokenizer" import {trackScope} from "./parserScopeContext" import {highlighting} from "./highlight" -const spec_Identifier = {__proto__:null,while:78, null:112, catch:118, finally:124, end:126, if:134, else:140, try:158, throw:162} +const spec_Identifier = {__proto__:null,while:78, null:112, catch:118, finally:124, end:126, if:134, else:140, try:158, throw:162, import:166} export const parser = LRParser.deserialize({ version: 14, - states: "UQQO,5:[O>ZQRO,59nO>bQRO,59nO:lQbO,5:hO>pQcO,5:jO@OQcO,5:jO@lQcO,5:jOOQa1G/_1G/_OOOO,59|,59|OOOO,59},59}OOOO-E8V-E8VOOQa1G/f1G/fOOQ`,5:Z,5:ZOOQ`-E8Y-E8YOOQa1G/}1G/}OBhQcO1G/}OBrQcO1G/}ODQQcO1G/}OD[QcO1G/}ODiQcO1G/}OOQa1G/[1G/[OEzQcO1G/[OFRQcO1G/[OFYQcO1G/[OGXQcO1G/[OFaQcO1G/[OOQ`-E8S-E8SOGoQRO1G/]OGyQQO1G/]OHOQQO1G/]OHWQQO1G/]OHcQRO1G/]OHjQRO1G/]OHqQbO,59rOH{QQO1G/]OOQa1G/]1G/]OITQQO1G0POOQa1G0Q1G0QOI`QbO1G0QOOQO'#E^'#E^OITQQO1G0POOQa1G0P1G0POOQ`'#E_'#E_OI`QbO1G0QOIjQbO1G0XOJUQbO1G0WOJpQbO'#DjOKRQbO'#DjOKfQbO1G0ROOQ`-E8R-E8ROOQ`,5:o,5:oOOQ`-E8T-E8TOKqQQO,59wOOQO,59x,59xOOQO-E8U-E8UOKyQbO1G/bO:lQbO1G/vO:lQbO1G/YOLQQbO1G0SOL]QQO7+$wOOQa7+$w7+$wOLeQQO1G/^OLmQQO7+%kOOQa7+%k7+%kOLxQbO7+%lOOQa7+%l7+%lOOQO-E8[-E8[OOQ`-E8]-E8]OOQ`'#EY'#EYOMSQQO'#EYOM[QbO'#ErOOQ`,5:U,5:UOMoQbO'#DhOMtQQO'#DkOOQ`7+%m7+%mOMyQbO7+%mONOQbO7+%mONWQbO7+$|ONfQbO7+$|ONvQbO7+%bO! OQbO7+$tOOQ`7+%n7+%nO! TQbO7+%nO! YQbO7+%nOOQa<sAN>sOOQ`AN>SAN>SO!#dQbOAN>SO!#iQbOAN>SOOQ`-E8Z-E8ZOOQ`AN>hAN>hO!#qQbOAN>hO2sQbO,5:_O:lQbO,5:aOOQ`AN>tAN>tPHqQbO'#EUOOQ`7+%Y7+%YOOQ`G23nG23nO!#vQbOG23nP!!vQbO'#DsOOQ`G24SG24SO!#{QQO1G/yOOQ`1G/{1G/{OOQ`LD)YLD)YO:lQbO7+%eOOQ`<`QRO'#EvOOQO'#Ev'#EvO>gQQO,5:[O>lQRO,59nO>sQRO,59nO:}QbO,5:hO?RQcO,5:jO@aQcO,5:jO@}QcO,5:jOArQbO,5:lOOQ`'#Eb'#EbO4tQbO,5:lOOQa1G/_1G/_OOOO,59|,59|OOOO,59},59}OOOO-E8X-E8XOOQa1G/f1G/fOOQ`,5:Z,5:ZOOQ`-E8[-E8[OOQa1G/}1G/}OCkQcO1G/}OCuQcO1G/}OETQcO1G/}OE_QcO1G/}OElQcO1G/}OOQa1G/[1G/[OF}QcO1G/[OGUQcO1G/[OG]QcO1G/[OH[QcO1G/[OGdQcO1G/[OOQ`-E8U-E8UOHrQRO1G/]OH|QQO1G/]OIRQQO1G/]OIZQQO1G/]OIfQRO1G/]OImQRO1G/]OItQbO,59rOJOQQO1G/]OOQa1G/]1G/]OJWQQO1G0POOQa1G0Q1G0QOJcQbO1G0QOOQO'#E`'#E`OJWQQO1G0POOQa1G0P1G0POOQ`'#Ea'#EaOJcQbO1G0QOJmQbO1G0ZOKXQbO1G0YOKsQbO'#DjOLUQbO'#DjOLiQbO1G0ROOQ`-E8T-E8TOOQ`,5:q,5:qOOQ`-E8V-E8VOLtQQO,59wOOQO,59x,59xOOQO-E8W-E8WOL|QbO1G/bO:}QbO1G/vO:}QbO1G/YOMTQbO1G0SOM`QbO1G0WOM}QbO1G0WOOQ`-E8`-E8`ONUQQO7+$wOOQa7+$w7+$wON^QQO1G/^ONfQQO7+%kOOQa7+%k7+%kONqQbO7+%lOOQa7+%l7+%lOOQO-E8^-E8^OOQ`-E8_-E8_OOQ`'#E['#E[ON{QQO'#E[O! TQbO'#EuOOQ`,5:U,5:UO! hQbO'#DhO! mQQO'#DkOOQ`7+%m7+%mO! rQbO7+%mO! wQbO7+%mO!!PQbO7+$|O!!_QbO7+$|O!!oQbO7+%bO!!wQbO7+$tOOQ`7+%n7+%nO!!|QbO7+%nO!#RQbO7+%nO!#ZQbO7+%rOOQa<sAN>sOOQ`AN>SAN>SO!%zQbOAN>SO!&PQbOAN>SOOQ`-E8]-E8]OOQ`AN>hAN>hO!&XQbOAN>hO2yQbO,5:_O:}QbO,5:aOOQ`AN>tAN>tPItQbO'#EWOOQ`7+%Y7+%YOOQ`G23nG23nO!&^QbOG23nP!%^QbO'#DsOOQ`G24SG24SO!&cQQO1G/yOOQ`1G/{1G/{OOQ`LD)YLD)YO:}QbO7+%eOOQ`<T!`#O$R#P;'S$R;'S;=`$j<%lO$RU>YV!TSOt$Ruw$Rx#O$R#P#Q>o#Q;'S$R;'S;=`$j<%lO$RU>vU#jQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$R~?_O#b~U?fU#lQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU@PU!TS!bQOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU@h^!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$RUAkU!RQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RUBS_!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#UCR#U#o@c#o;'S$R;'S;=`$j<%lO$RUCW`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#`@c#`#aDY#a#o@c#o;'S$R;'S;=`$j<%lO$RUD_`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#g@c#g#hEa#h#o@c#o;'S$R;'S;=`$j<%lO$RUEf`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#X@c#X#YFh#Y#o@c#o;'S$R;'S;=`$j<%lO$RUFo^!XQ!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$R^Gr^#cW!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$R^Hu^#eW!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$R^Ix`#dW!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#f@c#f#gJz#g#o@c#o;'S$R;'S;=`$j<%lO$RUKP`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#i@c#i#jEa#j#o@c#o;'S$R;'S;=`$j<%lO$RULYUuQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$R~LqO#m~", - tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#]~~", 11)], + repeatNodeCount: 13, + tokenData: "Lq~R!OOX$RXY$pYZ%ZZp$Rpq$pqr$Rrs%tst'ztu)cuw$Rwx)hxy)myz*Wz{$R{|*q|}$R}!O*q!O!P$R!P!Q4^!Q!R+c!R![.W![!]T!`#O$R#P;'S$R;'S;=`$j<%lO$RU>YV!TSOt$Ruw$Rx#O$R#P#Q>o#Q;'S$R;'S;=`$j<%lO$RU>vU#mQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$R~?_O#e~U?fU#oQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU@PU!TS!bQOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RU@h^!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$RUAkU!RQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$RUBS_!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#UCR#U#o@c#o;'S$R;'S;=`$j<%lO$RUCW`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#`@c#`#aDY#a#o@c#o;'S$R;'S;=`$j<%lO$RUD_`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#g@c#g#hEa#h#o@c#o;'S$R;'S;=`$j<%lO$RUEf`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#X@c#X#YFh#Y#o@c#o;'S$R;'S;=`$j<%lO$RUFo^!XQ!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$R^Gr^#fW!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$R^Hu^#hW!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#o@c#o;'S$R;'S;=`$j<%lO$R^Ix`#gW!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#f@c#f#gJz#g#o@c#o;'S$R;'S;=`$j<%lO$RUKP`!TSOt$Ruw$Rx}$R}!O@c!O!Q$R!Q![@c![!_$R!_!`Ad!`#O$R#P#T$R#T#i@c#i#jEa#j#o@c#o;'S$R;'S;=`$j<%lO$RULYUuQ!TSOt$Ruw$Rx#O$R#P;'S$R;'S;=`$j<%lO$R~LqO#p~", + tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#`~~", 11)], topRules: {"Program":[0,35]}, specialized: [{term: 28, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 28, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], - tokenPrec: 2256 + tokenPrec: 2370 }) diff --git a/src/parser/tests/basics.test.ts b/src/parser/tests/basics.test.ts index 28731f9..3558b5d 100644 --- a/src/parser/tests/basics.test.ts +++ b/src/parser/tests/basics.test.ts @@ -1017,3 +1017,34 @@ Assign `) }) }) + +describe('import', () => { + test('parses single import', () => { + expect(`import str`).toMatchTree(` + Import + keyword import + Identifier str + `) + }) + + test('parses multiple imports', () => { + expect(`import str math list`).toMatchTree(` + Import + keyword import + Identifier str + Identifier math + Identifier list + `) + }) + + test('parses named args', () => { + expect(`import str only=ends-with?`).toMatchTree(` + Import + keyword import + Identifier str + NamedArg + NamedArgPrefix only= + Identifier ends-with? + `) + }) +}) \ No newline at end of file diff --git a/src/prelude/index.ts b/src/prelude/index.ts index f92a2f5..8233834 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -45,6 +45,25 @@ export const globals = { return typeof v !== 'string' || this.scope.has(v) }, ref: (fn: Function) => fn, + import: function (this: VM, atNamed: Record = {}, ...idents: string[]) { + const onlyArray = Array.isArray(atNamed.only) ? atNamed.only : [atNamed.only].filter(a => a) + const only = new Set(onlyArray) + const wantsOnly = only.size > 0 + + + for (const ident of idents) { + const module = this.get(ident) + + if (!module) throw new Error(`import: can't find ${ident}`) + if (module.type !== 'dict') throw new Error(`import: can't import ${module.type}`) + + for (const [name, value] of module.value.entries()) { + if (value.type === 'dict') throw new Error(`import: can't import dicts in dicts`) + if (wantsOnly && !only.has(name)) continue + this.set(name, value) + } + } + }, // env args: Bun.argv.slice(1),