From c273429b24ba00f2cb99a2e408dc638d05e97b5a Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 7 Nov 2025 22:03:00 -0800 Subject: [PATCH] "add double quoted strings" --- src/compiler/tests/literals.test.ts | 21 +++++++++++ src/parser/shrimp.grammar | 3 +- src/parser/shrimp.terms.ts | 55 +++++++++++++++-------------- src/parser/shrimp.ts | 20 +++++------ src/parser/tests/strings.test.ts | 18 ++++++++++ 5 files changed, 79 insertions(+), 38 deletions(-) diff --git a/src/compiler/tests/literals.test.ts b/src/compiler/tests/literals.test.ts index 03858e3..bf6418d 100644 --- a/src/compiler/tests/literals.test.ts +++ b/src/compiler/tests/literals.test.ts @@ -200,3 +200,24 @@ describe('curly strings', () => { expect(`a = 1;b = 2;c = 3;{$a$b$c}`).toEvaluateTo(`123`) }) }) + +describe('double quoted strings', () => { + test("work", () => { + expect(`"hello world"`).toEvaluateTo('hello world') + }) + + test("don't interpolate", () => { + expect(`"hello $world"`).toEvaluateTo('hello $world') + expect(`"hello $(1 + 2)"`).toEvaluateTo('hello $(1 + 2)') + }) + + test("equal regular strings", () => { + expect(`"hello world" == 'hello world'`).toEvaluateTo(true) + }) + + test("can contain newlines", () => { + expect(` + "hello + world"`).toEvaluateTo('hello\n world') + }) +}) \ No newline at end of file diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 91a9faf..1535737 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -12,6 +12,7 @@ @precedence { Number Regex } StringFragment { !['\\$]+ } + DoubleQuote { '"' !["]* '"' } NamedArgPrefix { $[a-z-]+ "=" } Number { ("-" | "+")? $[0-9]+ ('.' $[0-9]+)? } Boolean { "true" | "false" } @@ -206,7 +207,7 @@ expression { } String { - "'" stringContent* "'" | CurlyString + "'" stringContent* "'" | CurlyString | DoubleQuote } } diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index b5b68e0..e6a6806 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -33,35 +33,36 @@ export const Number = 31, ParenExpr = 32, IfExpr = 33, - keyword = 71, + keyword = 72, ConditionalOp = 35, String = 36, StringFragment = 37, Interpolation = 38, EscapeSeq = 39, - Boolean = 40, - Regex = 41, - Dict = 42, - NamedArg = 43, - NamedArgPrefix = 44, - FunctionDef = 45, - Params = 46, - NamedParam = 47, - Null = 48, - colon = 49, - CatchExpr = 50, - Block = 52, - FinallyExpr = 53, - Underscore = 56, - Array = 57, - ElseIfExpr = 58, - ElseExpr = 60, - FunctionCallOrIdentifier = 61, - BinOp = 62, - PositionalArg = 63, - WhileExpr = 65, - FunctionCallWithBlock = 67, - TryExpr = 68, - Throw = 70, - CompoundAssign = 72, - Assign = 73 + DoubleQuote = 40, + Boolean = 41, + Regex = 42, + Dict = 43, + NamedArg = 44, + NamedArgPrefix = 45, + FunctionDef = 46, + Params = 47, + NamedParam = 48, + Null = 49, + colon = 50, + CatchExpr = 51, + Block = 53, + FinallyExpr = 54, + Underscore = 57, + Array = 58, + ElseIfExpr = 59, + ElseExpr = 61, + FunctionCallOrIdentifier = 62, + BinOp = 63, + PositionalArg = 64, + WhileExpr = 66, + FunctionCallWithBlock = 68, + TryExpr = 69, + Throw = 71, + CompoundAssign = 73, + Assign = 74 diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index ea588a3..8c74167 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,if:68, null:96, catch:102, finally:108, end:110, else:118, while:132, try:138, throw:142} +const spec_Identifier = {__proto__:null,if:68, null:98, catch:104, finally:110, end:112, else:120, while:134, try:140, throw:144} export const parser = LRParser.deserialize({ version: 14, - states: "9bQYQbOOO!gOSO'#DQOOQa'#DQ'#DQOOQa'#DW'#DWO#sQbO'#DgO%XQcO'#E_OOQa'#E_'#E_O&bQcO'#E_O'dQcO'#E^O'zQcO'#E^O)jQRO'#DPO*yQcO'#EXO+TQcO'#EXO+eQbO'#C|O,cOpO'#CzOOQ`'#EY'#EYO,hQbO'#EXO,rQRO'#DvOOQ`'#EX'#EXO-WQQO'#EWOOQ`'#EW'#EWOOQ`'#Dx'#DxQYQbOOO-`QbO'#DZO-kQbO'#C}O.cQbO'#DoO/ZQQO'#DrO.cQbO'#DtO/`QbO'#DSO/hQWO'#DTOOOO'#Ea'#EaOOOO'#Dy'#DyO/|OSO,59lOOQa,59l,59lOOQ`'#Dz'#DzO0[QbO,5:RO0cQbO'#DXO0mQQO,59rOOQa,5:R,5:RO0xQbO,5:ROOQa'#E^'#E^OOQ`'#Dm'#DmOOQ`'#Em'#EmOOQ`'#ER'#ERO1SQbO,59eO1|QbO,5:cO.cQbO,59kO.cQbO,59kO.cQbO,59kO.cQbO,5:WO.cQbO,5:WO.cQbO,5:WO2^QRO,59hO2eQRO,59hO2pQRO,59hO2kQQO,59hO3RQQO,59hO3ZObO,59fO3fQbO'#ESO3qQbO,59dO4]QbO,5:]O1|QbO,5:bOOQ`,5:r,5:rOOQ`-E7v-E7vOOQ`'#D{'#D{O4pQbO'#D[O4{QbO'#D]OOQO'#D|'#D|O4sQQO'#D[O5^QQO,59uO5cQcO'#E^O6wQRO'#E]O7OQRO'#E]OOQO'#E]'#E]O7ZQQO,59iO7`QRO,5:ZO7gQRO,5:ZO4]QbO,5:^O7rQcO,5:`O8nQcO,5:`O8xQcO,5:`OOOO,59n,59nOOOO,59o,59oOOOO-E7w-E7wOOQa1G/W1G/WOOQ`-E7x-E7xO9YQQO1G/^OOQa1G/m1G/mO9eQbO1G/mOOQ`,59s,59sOOQO'#EO'#EOO9YQQO1G/^OOQa1G/^1G/^OOQ`'#EP'#EPO9eQbO1G/mOOQ`-E8P-E8POOQ`1G/}1G/}OOQa1G/V1G/VO:pQcO1G/VO:wQcO1G/VO;OQcO1G/VOOQa1G/r1G/rO;wQcO1G/rOUQbO'#DbO>uQbO1G/wOOQ`1G/|1G/|OOQ`-E7y-E7yO?QQQO,59vOOQO,59w,59wOOQO-E7z-E7zO?YQbO1G/aO4]QbO1G/TO4]QbO1G/uO?mQbO1G/xO?xQQO7+$xOOQa7+$x7+$xO@TQbO7+%XOOQa7+%X7+%XOOQO-E7|-E7|OOQ`-E7}-E7}OOQ`'#D}'#D}O@_QQO'#D}O@dQbO'#EjOOQ`,59|,59|OATQbO'#D`OAYQQO'#DcOOQ`7+%c7+%cOA_QbO7+%cOAdQbO7+%cOAlQbO7+${OAwQbO7+${OBeQbO7+$oOBmQbO7+%aOOQ`7+%d7+%dOBrQbO7+%dOBwQbO7+%dOOQa<iAN>iOOQ`AN>RAN>ROD_QbOAN>RODdQbOAN>ROOQ`-E8O-E8OOOQ`AN=uAN=uODlQbOAN=uO-kQbO,5:SO4]QbO,5:UOOQ`AN>jAN>jOOQ`7+%Q7+%QOOQ`G23mG23mODqQbOG23mPDvQbO'#DhOOQ`G23aG23aOD{QQO1G/nOOQ`1G/p1G/pOOQ`LD)XLD)XO4]QbO7+%YOOQ`<aQbO'#CyOOQ`,5:o,5:oOOQ`-E8R-E8ROOQ`'#Dc'#DcO>nQbO'#DcO?_QbO1G/xOOQ`1G/}1G/}OOQ`-E7z-E7zO?jQQO,59wOOQO,59x,59xOOQO-E7{-E7{O?rQbO1G/bO4rQbO1G/TO4rQbO1G/vO@VQbO1G/yO@bQQO7+$yOOQa7+$y7+$yO@mQbO7+%YOOQa7+%Y7+%YOOQO-E7}-E7}OOQ`-E8O-E8OOOQ`'#EO'#EOO@wQQO'#EOO@|QbO'#EkOOQ`,59},59}OAmQbO'#DaOArQQO'#DdOOQ`7+%d7+%dOAwQbO7+%dOA|QbO7+%dOBUQbO7+$|OBaQbO7+$|OB}QbO7+$oOCVQbO7+%bOOQ`7+%e7+%eOC[QbO7+%eOCaQbO7+%eOOQa<jAN>jOOQ`AN>SAN>SODwQbOAN>SOD|QbOAN>SOOQ`-E8P-E8POOQ`AN=uAN=uOEUQbOAN=uO-zQbO,5:TO4rQbO,5:VOOQ`AN>kAN>kOOQ`7+%R7+%ROOQ`G23nG23nOEZQbOG23nPE`QbO'#DiOOQ`G23aG23aOEeQQO1G/oOOQ`1G/q1G/qOOQ`LD)YLD)YO4rQbO7+%ZOOQ`<c#Y#o,w#o;'S#{;'S;=`$d<%lO#{U>j[xQuSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^?g[#WWuSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^@d[#YWuSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^Aa^#XWuSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#f,w#f#gB]#g#o,w#o;'S#{;'S;=`$d<%lO#{UBb^uSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#i,w#i#j=b#j#o,w#o;'S#{;'S;=`$d<%lO#{UCeU!bQuSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~C|O#b~", - tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!}~~", 11)], + tokenData: "FV~R}OX$OXY$mYZ%WZp$Opq$mqr$Ors%qst'wtu)}uw$Owx*Sxy*Xyz*rz{$O{|+]|}$O}!O.P!O!P$O!P!Q0f!Q![+z![!]9R!]!^%W!^!}$O!}#O9l#O#P;b#P#Q;g#Q#R$O#R#Sj#a#o/Q#o;'S$O;'S;=`$g<%lO$OU>o^uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#g/Q#g#h?k#h#o/Q#o;'S$O;'S;=`$g<%lO$OU?p^uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#X/Q#X#Y@l#Y#o/Q#o;'S$O;'S;=`$g<%lO$OU@s[yQuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$O^Ap[#XWuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$O^Bm[#ZWuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#o/Q#o;'S$O;'S;=`$g<%lO$O^Cj^#YWuSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#f/Q#f#gDf#g#o/Q#o;'S$O;'S;=`$g<%lO$OUDk^uSOt$Ouw$Ox}$O}!O/Q!O!_$O!_!`/{!`#O$O#P#T$O#T#i/Q#i#j?k#j#o/Q#o;'S$O;'S;=`$g<%lO$OUEnU!cQuSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O~FVO#c~", + tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#O~~", 11)], topRules: {"Program":[0,27]}, specialized: [{term: 20, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 20, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], - tokenPrec: 1658 + tokenPrec: 1682 }) diff --git a/src/parser/tests/strings.test.ts b/src/parser/tests/strings.test.ts index 22f780b..7b4a672 100644 --- a/src/parser/tests/strings.test.ts +++ b/src/parser/tests/strings.test.ts @@ -157,4 +157,22 @@ describe('curly strings', () => { two { three } }`) }) +}) + +describe('double quoted strings', () => { + test("work", () => { + expect(`"hello world"`).toMatchTree(` + String + DoubleQuote "hello world"`) + }) + + test("don't interpolate", () => { + expect(`"hello $world"`).toMatchTree(` + String + DoubleQuote "hello $world"`) + + expect(`"hello $(1 + 2)"`).toMatchTree(` + String + DoubleQuote "hello $(1 + 2)"`) + }) }) \ No newline at end of file