From e2f5024a4c94e1cd61f09b65e8a7170415bbf748 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 9 Nov 2025 18:55:31 -0800 Subject: [PATCH 1/5] add `import` keyword for importing keys of dicts into local scope --- src/compiler/compiler.ts | 17 +++++++++++++++++ src/compiler/tests/compiler.test.ts | 18 ++++++++++++++++++ src/parser/shrimp.grammar | 7 +++++++ src/parser/shrimp.terms.ts | 7 ++++--- src/parser/shrimp.ts | 20 ++++++++++---------- src/parser/tests/basics.test.ts | 20 ++++++++++++++++++++ src/prelude/index.ts | 13 +++++++++++++ 7 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 80ce825..a96155d 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -797,6 +797,23 @@ export class Compiler { return instructions } + case terms.Import: { + const instructions: ProgramItem[] = [] + const [_import, ...dicts] = getAllChildren(node) + + instructions.push(['LOAD', 'import']) + + dicts.forEach((dict) => + instructions.push(['PUSH', input.slice(dict.from, dict.to)]) + ) + + instructions.push(['PUSH', dicts.length]) + instructions.push(['PUSH', 0]) + 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..5194300 100644 --- a/src/compiler/tests/compiler.test.ts +++ b/src/compiler/tests/compiler.test.ts @@ -448,3 +448,21 @@ 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') + }) +}) \ No newline at end of file diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 506f036..8857fce 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 AssignableIdentifier+ +} + 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..41e9d73 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>dQQO,5:[O>iQRO,59nO>pQRO,59nO:zQbO,5:hO?OQcO,5:jO@^QcO,5:jO@zQcO,5:jOOQ`'#Eb'#EbOAoQbO,5:lOOQa1G/_1G/_OOOO,59|,59|OOOO,59},59}OOOO-E8X-E8XOOQa1G/f1G/fOOQ`,5:Z,5:ZOOQ`-E8[-E8[OOQa1G/}1G/}OCeQcO1G/}OCoQcO1G/}OD}QcO1G/}OEXQcO1G/}OEfQcO1G/}OOQa1G/[1G/[OFwQcO1G/[OGOQcO1G/[OGVQcO1G/[OHUQcO1G/[OG^QcO1G/[OOQ`-E8U-E8UOHlQRO1G/]OHvQQO1G/]OH{QQO1G/]OITQQO1G/]OI`QRO1G/]OIgQRO1G/]OInQbO,59rOIxQQO1G/]OOQa1G/]1G/]OJQQQO1G0POOQa1G0Q1G0QOJ]QbO1G0QOOQO'#E`'#E`OJQQQO1G0POOQa1G0P1G0POOQ`'#Ea'#EaOJ]QbO1G0QOJgQbO1G0ZOKRQbO1G0YOKmQbO'#DjOLOQbO'#DjOLcQbO1G0ROOQ`-E8T-E8TOOQ`,5:q,5:qOOQ`-E8V-E8VOLnQQO,59wOOQO,59x,59xOOQO-E8W-E8WOLvQbO1G/bO:zQbO1G/vO:zQbO1G/YOL}QbO1G0SOOQ`-E8`-E8`OMYQQO7+$wOOQa7+$w7+$wOMbQQO1G/^OMjQQO7+%kOOQa7+%k7+%kOMuQbO7+%lOOQa7+%l7+%lOOQO-E8^-E8^OOQ`-E8_-E8_OOQ`'#E['#E[ONPQQO'#E[ONXQbO'#EuOOQ`,5:U,5:UONlQbO'#DhONqQQO'#DkOOQ`7+%m7+%mONvQbO7+%mON{QbO7+%mO! TQbO7+$|O! cQbO7+$|O! sQbO7+%bO! {QbO7+$tOOQ`7+%n7+%nO!!QQbO7+%nO!!VQbO7+%nOOQa<sAN>sOOQ`AN>SAN>SO!$aQbOAN>SO!$fQbOAN>SOOQ`-E8]-E8]OOQ`AN>hAN>hO!$nQbOAN>hO2yQbO,5:_O:zQbO,5:aOOQ`AN>tAN>tPInQbO'#EWOOQ`7+%Y7+%YOOQ`G23nG23nO!$sQbOG23nP!#sQbO'#DsOOQ`G24SG24SO!$xQQO1G/yOOQ`1G/{1G/{OOQ`LD)YLD)YO:zQbO7+%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: 2299 }) diff --git a/src/parser/tests/basics.test.ts b/src/parser/tests/basics.test.ts index 28731f9..b9e7c76 100644 --- a/src/parser/tests/basics.test.ts +++ b/src/parser/tests/basics.test.ts @@ -1017,3 +1017,23 @@ Assign `) }) }) + +describe('import', () => { + test('parses single import', () => { + expect(`import str`).toMatchTree(` + Import + keyword import + AssignableIdentifier str + `) + }) + + test('parses multiple imports', () => { + expect(`import str math list`).toMatchTree(` + Import + keyword import + AssignableIdentifier str + AssignableIdentifier math + AssignableIdentifier list + `) + }) +}) \ No newline at end of file diff --git a/src/prelude/index.ts b/src/prelude/index.ts index f92a2f5..4097565 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -45,6 +45,19 @@ export const globals = { return typeof v !== 'string' || this.scope.has(v) }, ref: (fn: Function) => fn, + import: function (this: VM, ...idents: string[]) { + 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`) + this.set(name, value) + } + } + }, // env args: Bun.argv.slice(1), From 970ceeb8b0b3a17465d1bf269ffb4f00b3dbc6d5 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 9 Nov 2025 19:20:48 -0800 Subject: [PATCH 2/5] import dict only=something --- src/compiler/compiler.ts | 16 ++++++++++++---- src/compiler/tests/compiler.test.ts | 10 ++++++++++ src/parser/shrimp.grammar | 2 +- src/parser/shrimp.ts | 8 ++++---- src/parser/tests/basics.test.ts | 19 +++++++++++++++---- src/prelude/index.ts | 8 +++++++- 6 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index a96155d..d52576e 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -799,16 +799,24 @@ export class Compiler { case terms.Import: { const instructions: ProgramItem[] = [] - const [_import, ...dicts] = getAllChildren(node) + 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']) - dicts.forEach((dict) => + args.forEach((dict) => instructions.push(['PUSH', input.slice(dict.from, dict.to)]) ) - instructions.push(['PUSH', dicts.length]) - instructions.push(['PUSH', 0]) + 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 diff --git a/src/compiler/tests/compiler.test.ts b/src/compiler/tests/compiler.test.ts index 5194300..45fdb39 100644 --- a/src/compiler/tests/compiler.test.ts +++ b/src/compiler/tests/compiler.test.ts @@ -465,4 +465,14 @@ describe('import', () => { 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 8857fce..80adcdb 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -165,7 +165,7 @@ Throw { // this has to be in the parse tree so the scope tracker can use it Import { - import AssignableIdentifier+ + import NamedArg* Identifier+ NamedArg* } ConditionalOp { diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 41e9d73..051f00e 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -7,9 +7,9 @@ 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, import:166} export const parser = LRParser.deserialize({ version: 14, - states: "=dQYQbOOO!mOpO'#DXO!rOSO'#D`OOQa'#D`'#D`O%mQcO'#DvO(mQcO'#EiOOQ`'#Ew'#EwO)WQRO'#DwO+]QcO'#EgO+vQbO'#DVOOQa'#Dy'#DyO.[QbO'#DzOOQa'#Ei'#EiO.cQcO'#EiO0aQcO'#EhO1fQcO'#EgO1sQRO'#ESOOQ`'#Eg'#EgO2[QbO'#EgO2cQQO'#EfOOQ`'#Ef'#EfOOQ`'#EU'#EUQYQbOOO2nQbO'#D[O2yQbO'#DpO3tQbO'#DSO4oQQO'#D|O3tQbO'#EOO4tQbO'#EQO4yObO,59sO5UQbO'#DbO5^QWO'#DcOOOO'#Eo'#EoOOOO'#EZ'#EZO5rOSO,59zOOQa,59z,59zOOQ`'#DZ'#DZO6QQbO'#DoOOQ`'#Em'#EmOOQ`'#E^'#E^O6[QbO,5:^OOQa'#Eh'#EhO3tQbO,5:cO3tQbO,5:cO3tQbO,5:cO3tQbO,5:cO3tQbO,59pO3tQbO,59pO3tQbO,59pO3tQbO,59pOOQ`'#EW'#EWO+vQbO,59qO7UQcO'#DvO7]QcO'#EiO7dQRO,59qO7nQQO,59qO7sQQO,59qO7{QQO,59qO8WQRO,59qO8pQRO,59qO8wQQO'#DQO8|QbO,5:fO9TQQO,5:eOOQa,5:f,5:fO9`QbO,5:fO9jQbO,5:oO9jQbO,5:nO:zQbO,5:gO;RQbO,59lOOQ`,5;Q,5;QO9jQbO'#EVOOQ`-E8S-E8SOOQ`'#EX'#EXO;mQbO'#D]O;xQbO'#D^OOQO'#EY'#EYO;pQQO'#D]O<^QQO,59vO]QRO'#EvOOQO'#Ev'#EvO>dQQO,5:[O>iQRO,59nO>pQRO,59nO:zQbO,5:hO?OQcO,5:jO@^QcO,5:jO@zQcO,5:jOOQ`'#Eb'#EbOAoQbO,5:lOOQa1G/_1G/_OOOO,59|,59|OOOO,59},59}OOOO-E8X-E8XOOQa1G/f1G/fOOQ`,5:Z,5:ZOOQ`-E8[-E8[OOQa1G/}1G/}OCeQcO1G/}OCoQcO1G/}OD}QcO1G/}OEXQcO1G/}OEfQcO1G/}OOQa1G/[1G/[OFwQcO1G/[OGOQcO1G/[OGVQcO1G/[OHUQcO1G/[OG^QcO1G/[OOQ`-E8U-E8UOHlQRO1G/]OHvQQO1G/]OH{QQO1G/]OITQQO1G/]OI`QRO1G/]OIgQRO1G/]OInQbO,59rOIxQQO1G/]OOQa1G/]1G/]OJQQQO1G0POOQa1G0Q1G0QOJ]QbO1G0QOOQO'#E`'#E`OJQQQO1G0POOQa1G0P1G0POOQ`'#Ea'#EaOJ]QbO1G0QOJgQbO1G0ZOKRQbO1G0YOKmQbO'#DjOLOQbO'#DjOLcQbO1G0ROOQ`-E8T-E8TOOQ`,5:q,5:qOOQ`-E8V-E8VOLnQQO,59wOOQO,59x,59xOOQO-E8W-E8WOLvQbO1G/bO:zQbO1G/vO:zQbO1G/YOL}QbO1G0SOOQ`-E8`-E8`OMYQQO7+$wOOQa7+$w7+$wOMbQQO1G/^OMjQQO7+%kOOQa7+%k7+%kOMuQbO7+%lOOQa7+%l7+%lOOQO-E8^-E8^OOQ`-E8_-E8_OOQ`'#E['#E[ONPQQO'#E[ONXQbO'#EuOOQ`,5:U,5:UONlQbO'#DhONqQQO'#DkOOQ`7+%m7+%mONvQbO7+%mON{QbO7+%mO! TQbO7+$|O! cQbO7+$|O! sQbO7+%bO! {QbO7+$tOOQ`7+%n7+%nO!!QQbO7+%nO!!VQbO7+%nOOQa<sAN>sOOQ`AN>SAN>SO!$aQbOAN>SO!$fQbOAN>SOOQ`-E8]-E8]OOQ`AN>hAN>hO!$nQbOAN>hO2yQbO,5:_O:zQbO,5:aOOQ`AN>tAN>tPInQbO'#EWOOQ`7+%Y7+%YOOQ`G23nG23nO!$sQbOG23nP!#sQbO'#DsOOQ`G24SG24SO!$xQQO1G/yOOQ`1G/{1G/{OOQ`LD)YLD)YO:zQbO7+%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`< (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 28, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], - tokenPrec: 2299 + tokenPrec: 2370 }) diff --git a/src/parser/tests/basics.test.ts b/src/parser/tests/basics.test.ts index b9e7c76..3558b5d 100644 --- a/src/parser/tests/basics.test.ts +++ b/src/parser/tests/basics.test.ts @@ -1023,7 +1023,7 @@ describe('import', () => { expect(`import str`).toMatchTree(` Import keyword import - AssignableIdentifier str + Identifier str `) }) @@ -1031,9 +1031,20 @@ describe('import', () => { expect(`import str math list`).toMatchTree(` Import keyword import - AssignableIdentifier str - AssignableIdentifier math - AssignableIdentifier list + 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 4097565..8233834 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -45,7 +45,12 @@ export const globals = { return typeof v !== 'string' || this.scope.has(v) }, ref: (fn: Function) => fn, - import: function (this: VM, ...idents: string[]) { + 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) @@ -54,6 +59,7 @@ export const globals = { 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) } } From f58ff1785ae52a3c71c2635a8802b6de655e48d8 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 9 Nov 2025 19:45:02 -0800 Subject: [PATCH 3/5] dont print eval result --- bin/shrimp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/shrimp b/bin/shrimp index d26ec68..f75e6b2 100755 --- a/bin/shrimp +++ b/bin/shrimp @@ -65,8 +65,8 @@ async function main() { try { mkdirSync('/tmp/shrimp') } catch { } const path = `/tmp/shrimp/${randomUUID()}.sh` - writeFileSync(path, code) - console.log(await runFile(path)) + writeFileSync(path, fullCode) + await runFile(path) return } From 9eaa71fe2df243f932ef26bbf71f2f8814afc631 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 9 Nov 2025 19:46:54 -0800 Subject: [PATCH 4/5] cli: Add -I (import) --- bin/shrimp | 50 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/bin/shrimp b/bin/shrimp index f75e6b2..49d1d78 100755 --- a/bin/shrimp +++ b/bin/shrimp @@ -12,16 +12,21 @@ 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}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() { @@ -29,7 +34,40 @@ function showVersion() { } 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,6 +101,10 @@ async function main() { process.exit(1) } + // Prepend import statement if -I flags were provided + const importStatement = imports.length > 0 ? `import ${imports.join(' ')}` : '' + const fullCode = importStatement ? `${importStatement}; ${code}` : code + try { mkdirSync('/tmp/shrimp') } catch { } const path = `/tmp/shrimp/${randomUUID()}.sh` writeFileSync(path, fullCode) From 10e1986fe22344134846e6d170bfdb6ae9e6a7b6 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 9 Nov 2025 19:52:59 -0800 Subject: [PATCH 5/5] cli: add print/-E --- bin/shrimp | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/bin/shrimp b/bin/shrimp index 49d1d78..fe856b3 100755 --- a/bin/shrimp +++ b/bin/shrimp @@ -2,10 +2,9 @@ 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' @@ -19,6 +18,7 @@ ${colors.bright}Commands:${colors.reset} ${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}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 @@ -33,6 +33,12 @@ 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() { let args = process.argv.slice(2) @@ -101,14 +107,18 @@ async function main() { process.exit(1) } - // Prepend import statement if -I flags were provided - const importStatement = imports.length > 0 ? `import ${imports.join(' ')}` : '' - const fullCode = importStatement ? `${importStatement}; ${code}` : code + await evalCode(code, imports) + return + } - try { mkdirSync('/tmp/shrimp') } catch { } - const path = `/tmp/shrimp/${randomUUID()}.sh` - writeFileSync(path, fullCode) - await runFile(path) + 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 }