From bc0684185a1179c7d48dcc360fbc486a99358266 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 29 Oct 2025 15:49:59 -0700 Subject: [PATCH 1/2] Add += and friends --- src/compiler/compiler.ts | 29 +++++++++++ src/compiler/tests/compiler.test.ts | 32 ++++++++++++ src/compiler/utils.ts | 21 ++++++++ src/parser/operatorTokenizer.ts | 9 +++- src/parser/shrimp.grammar | 7 ++- src/parser/shrimp.terms.ts | 80 ++++++++++++++++------------- src/parser/shrimp.ts | 24 ++++----- src/parser/tests/basics.test.ts | 66 ++++++++++++++++++++++++ src/parser/tokenizer.ts | 11 ++++ 9 files changed, 228 insertions(+), 51 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 72b0f23..02eb635 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -9,6 +9,7 @@ import { checkTreeForErrors, getAllChildren, getAssignmentParts, + getCompoundAssignmentParts, getBinaryParts, getDotGetParts, getFunctionCallParts, @@ -245,6 +246,34 @@ export class Compiler { return instructions } + case terms.CompoundAssign: { + const { identifier, operator, right } = getCompoundAssignmentParts(node) + const identifierName = input.slice(identifier.from, identifier.to) + const instructions: ProgramItem[] = [] + + // will throw if undefined + instructions.push(['LOAD', identifierName]) + + instructions.push(...this.#compileNode(right, input)) + + const opValue = input.slice(operator.from, operator.to) + switch (opValue) { + case '+=': instructions.push(['ADD']); break + case '-=': instructions.push(['SUB']); break + case '*=': instructions.push(['MUL']); break + case '/=': instructions.push(['DIV']); break + case '%=': instructions.push(['MOD']); break + default: + throw new CompilerError(`Unknown compound operator: ${opValue}`, operator.from, operator.to) + } + + // DUP and store (same as regular assignment) + instructions.push(['DUP']) + instructions.push(['STORE', identifierName]) + + return instructions + } + case terms.ParenExpr: { const child = node.firstChild if (!child) return [] // I guess it is empty parentheses? diff --git a/src/compiler/tests/compiler.test.ts b/src/compiler/tests/compiler.test.ts index 4d662c3..7a67a9e 100644 --- a/src/compiler/tests/compiler.test.ts +++ b/src/compiler/tests/compiler.test.ts @@ -64,6 +64,38 @@ describe('compiler', () => { expect('sum = 2 + 3; sum').toEvaluateTo(5) }) + test('compound assignment +=', () => { + expect('x = 10; x += 5; x').toEvaluateTo(15) + }) + + test('compound assignment -= ', () => { + expect('x = 10; x -= 3; x').toEvaluateTo(7) + }) + + test('compound assignment *=', () => { + expect('x = 5; x *= 3; x').toEvaluateTo(15) + }) + + test('compound assignment /=', () => { + expect('x = 20; x /= 4; x').toEvaluateTo(5) + }) + + test('compound assignment %=', () => { + expect('x = 17; x %= 5; x').toEvaluateTo(2) + }) + + test('compound assignment with expression', () => { + expect('x = 10; x += 2 + 3; x').toEvaluateTo(15) + }) + + test('compound assignment returns value', () => { + expect('x = 5; x += 10; x').toEvaluateTo(15) + }) + + test('compound assignment fails on undefined variable', () => { + expect('undefined-var += 5').toFailEvaluation() + }) + test('parentheses', () => { expect('(2 + 3) * 4').toEvaluateTo(20) }) diff --git a/src/compiler/utils.ts b/src/compiler/utils.ts index 82b4025..60ab836 100644 --- a/src/compiler/utils.ts +++ b/src/compiler/utils.ts @@ -57,6 +57,27 @@ export const getAssignmentParts = (node: SyntaxNode) => { return { identifier: left, right } } +export const getCompoundAssignmentParts = (node: SyntaxNode) => { + const children = getAllChildren(node) + const [left, operator, right] = children + + if (!left || left.type.id !== terms.AssignableIdentifier) { + throw new CompilerError( + `CompoundAssign left child must be an AssignableIdentifier, got ${left ? left.type.name : 'none'}`, + node.from, + node.to + ) + } else if (!operator || !right) { + throw new CompilerError( + `CompoundAssign expected 3 children, got ${children.length}`, + node.from, + node.to + ) + } + + return { identifier: left, operator, right } +} + export const getFunctionDefParts = (node: SyntaxNode, input: string) => { const children = getAllChildren(node) const [fnKeyword, paramsNode, colon, ...bodyNodes] = children diff --git a/src/parser/operatorTokenizer.ts b/src/parser/operatorTokenizer.ts index ee1dc44..3c85400 100644 --- a/src/parser/operatorTokenizer.ts +++ b/src/parser/operatorTokenizer.ts @@ -10,7 +10,14 @@ const operators: Array = [ { str: '!=', tokenName: 'Neq' }, { str: '==', tokenName: 'EqEq' }, - // // Single-char operators + // Compound assignment operators (must come before single-char operators) + { str: '+=', tokenName: 'PlusEq' }, + { str: '-=', tokenName: 'MinusEq' }, + { str: '*=', tokenName: 'StarEq' }, + { str: '/=', tokenName: 'SlashEq' }, + { str: '%=', tokenName: 'ModuloEq' }, + + // Single-char operators { str: '*', tokenName: 'Star' }, { str: '=', tokenName: 'Eq' }, { str: '/', tokenName: 'Slash' }, diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 635035a..641bf02 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -6,7 +6,7 @@ @top Program { item* } -@external tokens operatorTokenizer from "./operatorTokenizer" { Star, Slash, Plus, Minus, And, Or, Eq, EqEq, Neq, Lt, Lte, Gt, Gte, Modulo } +@external tokens operatorTokenizer from "./operatorTokenizer" { Star, Slash, Plus, Minus, And, Or, Eq, EqEq, Neq, Lt, Lte, Gt, Gte, Modulo, PlusEq, MinusEq, StarEq, SlashEq, ModuloEq } @tokens { @precedence { Number Regex } @@ -53,6 +53,7 @@ consumeToTerminator { ambiguousFunctionCall | IfExpr | FunctionDef | + CompoundAssign | Assign | BinOp | ConditionalOp | @@ -151,6 +152,10 @@ Assign { AssignableIdentifier Eq consumeToTerminator } +CompoundAssign { + AssignableIdentifier (PlusEq | MinusEq | StarEq | SlashEq | ModuloEq) consumeToTerminator +} + BinOp { expression !multiplicative Modulo expression | (expression | BinOp) !multiplicative Star (expression | BinOp) | diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index 144d69b..54fae8a 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -14,40 +14,46 @@ export const Gt = 12, Gte = 13, Modulo = 14, - Identifier = 15, - AssignableIdentifier = 16, - Word = 17, - IdentifierBeforeDot = 18, - Do = 19, - Program = 20, - PipeExpr = 21, - FunctionCall = 22, - DotGet = 23, - Number = 24, - ParenExpr = 25, - FunctionCallOrIdentifier = 26, - BinOp = 27, - String = 28, - StringFragment = 29, - Interpolation = 30, - EscapeSeq = 31, - Boolean = 32, - Regex = 33, - Dict = 34, - NamedArg = 35, - NamedArgPrefix = 36, - FunctionDef = 37, - Params = 38, - colon = 39, - keyword = 54, - Underscore = 41, - Array = 42, - Null = 43, - ConditionalOp = 44, - PositionalArg = 45, - IfExpr = 47, - SingleLineThenBlock = 49, - ThenBlock = 50, - ElseIfExpr = 51, - ElseExpr = 53, - Assign = 55 + PlusEq = 15, + MinusEq = 16, + StarEq = 17, + SlashEq = 18, + ModuloEq = 19, + Identifier = 20, + AssignableIdentifier = 21, + Word = 22, + IdentifierBeforeDot = 23, + Do = 24, + Program = 25, + PipeExpr = 26, + FunctionCall = 27, + DotGet = 28, + Number = 29, + ParenExpr = 30, + FunctionCallOrIdentifier = 31, + BinOp = 32, + String = 33, + StringFragment = 34, + Interpolation = 35, + EscapeSeq = 36, + Boolean = 37, + Regex = 38, + Dict = 39, + NamedArg = 40, + NamedArgPrefix = 41, + FunctionDef = 42, + Params = 43, + colon = 44, + keyword = 59, + Underscore = 46, + Array = 47, + Null = 48, + ConditionalOp = 49, + PositionalArg = 50, + IfExpr = 52, + SingleLineThenBlock = 54, + ThenBlock = 55, + ElseIfExpr = 56, + ElseExpr = 58, + CompoundAssign = 60, + Assign = 61 diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 6e19a7f..2f79c7c 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 "./scopeTracker" import {highlighting} from "./highlight" -const spec_Identifier = {__proto__:null,end:80, null:86, if:96, elseif:104, else:108} +const spec_Identifier = {__proto__:null,end:90, null:96, if:106, elseif:114, else:118} export const parser = LRParser.deserialize({ version: 14, - states: "3UQYQbOOO#hQcO'#CvO$eOSO'#CxO$sQbO'#EVOOQ`'#DR'#DROOQa'#DO'#DOO%vQbO'#DWO&{QcO'#DzOOQa'#Dz'#DzO)PQcO'#DyO)xQRO'#CwO*]QcO'#DuO*tQcO'#DuO+VQbO'#CuO+}OpO'#CsOOQ`'#Dv'#DvO,SQbO'#DuO,bQbO'#E]OOQ`'#D]'#D]O-VQRO'#DeOOQ`'#Du'#DuO-[QQO'#DtOOQ`'#Dt'#DtOOQ`'#Df'#DfQYQbOOO-dQbO'#DPOOQa'#Dy'#DyOOQ`'#DZ'#DZOOQ`'#E['#E[OOQ`'#Dm'#DmO-nQbO,59^O.RQbO'#CzO.ZQWO'#C{OOOO'#D|'#D|OOOO'#Dg'#DgO.oOSO,59dOOQa,59d,59dOOQ`'#Di'#DiO.}QbO'#DSO/VQQO,5:qOOQ`'#Dh'#DhO/[QbO,59rO/cQQO,59jOOQa,59r,59rO/nQbO,59rO,bQbO,59cO,bQbO,59cO,bQbO,59cO,bQbO,59tO,bQbO,59tO,bQbO,59tO/xQRO,59aO0PQRO,59aO0bQRO,59aO0]QQO,59aO0mQQO,59aO0uObO,59_O1QQbO'#DnO1]QbO,59]O1nQRO,5:wO1uQRO,5:wO2QQbO,5:POOQ`,5:`,5:`OOQ`-E7d-E7dOOQ`,59k,59kOOQ`-E7k-E7kOOOO,59f,59fOOOO,59g,59gOOOO-E7e-E7eOOQa1G/O1G/OOOQ`-E7g-E7gO2[QbO1G0]OOQ`-E7f-E7fO2iQQO1G/UOOQa1G/^1G/^O2tQbO1G/^OOQO'#Dk'#DkO2iQQO1G/UOOQa1G/U1G/UOOQ`'#Dl'#DlO2tQbO1G/^OOQa1G.}1G.}O3gQcO1G.}O3qQcO1G.}O3{QcO1G.}OOQa1G/`1G/`O5_QcO1G/`O5fQcO1G/`O5mQcO1G/`OOQa1G.{1G.{OOQa1G.y1G.yO!ZQbO'#CvO%}QbO'#CrOOQ`,5:Y,5:YOOQ`-E7l-E7lO5tQbO1G0cOOQ`1G/k1G/kO6RQbO7+%wO6WQbO7+%xO6hQQO7+$pOOQa7+$p7+$pO6sQbO7+$xOOQa7+$x7+$xOOQO-E7i-E7iOOQ`-E7j-E7jOOQ`'#D_'#D_O6}QbO7+%}O7SQbO7+&OOOQ`<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#Yo[!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!k~~", 11)], - topRules: {"Program":[0,20]}, - 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: 1135 + 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#ch#i#o7^#o#p#{#p#q@`#q;'S#{;'S;=`$d<%l~#{~O#{~~@yS$QUrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUrS!lYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UrS#OQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZrS!mYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!mYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O!w~~'aO!u~U'hUrS!rQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUrS#TQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWrSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZYrSmQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OWrSOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oWrSmQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^WrSOt#{uw#{x!P#{!P!Q+v!Q#O#{#P;'S#{;'S;=`$d<%lO#{U+{^rSOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q#{!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wU-O^rSvQOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q0o!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wQ.PXvQOY-zZ!P-z!P!Q.l!Q!}-z!}#O/Z#O#P0Y#P;'S-z;'S;=`0i<%lO-zQ.oP!P!Q.rQ.wUvQ#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-zU0tWrSOt#{uw#{x!P#{!P!Q1^!Q#O#{#P;'S#{;'S;=`$d<%lO#{U1ebrSvQOt#{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[rSOY2mYZ#{Zt2mtu/Zuw2mwx/Zx#O2m#O#P/s#P#Q,w#Q;'S2m;'S;=`3h<%lO2mU3kP;=`<%l2mU3qP;=`<%l,wU3{UrS|QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4fW!}QrSOt#{uw#{x!_#{!_!`5O!`#O#{#P;'S#{;'S;=`$d<%lO#{U5TVrSOt#{uw#{x#O#{#P#Q5j#Q;'S#{;'S;=`$d<%lO#{U5qU!|QrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6YO!x~U6aU#SQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6zUrS!OQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7cYrSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{U8YUyQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U8qZrSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#U9d#U#o7^#o;'S#{;'S;=`$d<%lO#{U9i[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#`7^#`#a:_#a#o7^#o;'S#{;'S;=`$d<%lO#{U:d[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#g7^#g#h;Y#h#o7^#o;'S#{;'S;=`$d<%lO#{U;_[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#X7^#X#Yo[!zWrSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#f7^#f#g?e#g#o7^#o;'S#{;'S;=`$d<%lO#{U?j[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#i7^#i#j;Y#j#o7^#o;'S#{;'S;=`$d<%lO#{U@gU!TQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~AOO#X~", + tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!q~~", 11)], + topRules: {"Program":[0,25]}, + 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: 1150 }) diff --git a/src/parser/tests/basics.test.ts b/src/parser/tests/basics.test.ts index da6d4bb..86e4a97 100644 --- a/src/parser/tests/basics.test.ts +++ b/src/parser/tests/basics.test.ts @@ -532,6 +532,72 @@ describe('Assign', () => { }) }) +describe('CompoundAssign', () => { + test('parses += operator', () => { + expect('x += 5').toMatchTree(` + CompoundAssign + AssignableIdentifier x + PlusEq += + Number 5`) + }) + + test('parses -= operator', () => { + expect('count -= 1').toMatchTree(` + CompoundAssign + AssignableIdentifier count + MinusEq -= + Number 1`) + }) + + test('parses *= operator', () => { + expect('total *= 2').toMatchTree(` + CompoundAssign + AssignableIdentifier total + StarEq *= + Number 2`) + }) + + test('parses /= operator', () => { + expect('value /= 10').toMatchTree(` + CompoundAssign + AssignableIdentifier value + SlashEq /= + Number 10`) + }) + + test('parses %= operator', () => { + expect('remainder %= 3').toMatchTree(` + CompoundAssign + AssignableIdentifier remainder + ModuloEq %= + Number 3`) + }) + + test('parses compound assignment with expression', () => { + expect('x += 1 + 2').toMatchTree(` + CompoundAssign + AssignableIdentifier x + PlusEq += + BinOp + Number 1 + Plus + + Number 2`) + }) + + test('parses compound assignment with function call', () => { + expect('total += add 5 3').toMatchTree(` + CompoundAssign + AssignableIdentifier total + PlusEq += + FunctionCall + Identifier add + PositionalArg + Number 5 + PositionalArg + Number 3`) + }) +}) + describe('DotGet whitespace sensitivity', () => { test('no whitespace - DotGet works when identifier in scope', () => { expect('basename = 5; basename.prop').toMatchTree(` diff --git a/src/parser/tokenizer.ts b/src/parser/tokenizer.ts index e4fc895..cbaac67 100644 --- a/src/parser/tokenizer.ts +++ b/src/parser/tokenizer.ts @@ -184,6 +184,17 @@ const chooseIdentifierToken = (input: InputStream, stack: Stack): number => { } const nextCh = getFullCodePoint(input, peekPos) + const nextCh2 = getFullCodePoint(input, peekPos + 1) + + // Check for compound assignment operators: +=, -=, *=, /=, %= + if ([43/* + */, 45/* - */, 42/* * */, 47/* / */, 37/* % */].includes(nextCh) && nextCh2 === 61/* = */) { + // Found compound operator, check if it's followed by whitespace + const charAfterOp = getFullCodePoint(input, peekPos + 2) + if (isWhiteSpace(charAfterOp) || charAfterOp === -1 /* EOF */) { + return AssignableIdentifier + } + } + if (nextCh === 61 /* = */) { // Found '=', but check if it's followed by whitespace // If '=' is followed by non-whitespace (like '=cool*'), it won't be tokenized as Eq -- 2.50.1 From 887be4124809c84c7f6d7b276ac2fa4a4175836c Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 31 Oct 2025 10:06:47 -0700 Subject: [PATCH 2/2] Update generated files --- src/parser/shrimp.terms.ts | 31 ++++++++++++++++++------------- src/parser/shrimp.ts | 18 +++++++++--------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index 54fae8a..ab4011b 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -44,16 +44,21 @@ export const FunctionDef = 42, Params = 43, colon = 44, - keyword = 59, - Underscore = 46, - Array = 47, - Null = 48, - ConditionalOp = 49, - PositionalArg = 50, - IfExpr = 52, - SingleLineThenBlock = 54, - ThenBlock = 55, - ElseIfExpr = 56, - ElseExpr = 58, - CompoundAssign = 60, - Assign = 61 + CatchExpr = 45, + keyword = 68, + TryBlock = 47, + FinallyExpr = 48, + Underscore = 51, + Array = 52, + Null = 53, + ConditionalOp = 54, + PositionalArg = 55, + TryExpr = 57, + Throw = 59, + IfExpr = 61, + SingleLineThenBlock = 63, + ThenBlock = 64, + ElseIfExpr = 65, + ElseExpr = 67, + CompoundAssign = 69, + Assign = 70 diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 2f79c7c..172f6a7 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -4,14 +4,14 @@ import {operatorTokenizer} from "./operatorTokenizer" import {tokenizer, specializeKeyword} from "./tokenizer" import {trackScope} from "./scopeTracker" import {highlighting} from "./highlight" -const spec_Identifier = {__proto__:null,end:90, null:96, if:106, elseif:114, else:118} +const spec_Identifier = {__proto__:null,catch:92, finally:98, end:100, null:106, try:116, throw:120, if:124, elseif:132, else:136} export const parser = LRParser.deserialize({ version: 14, - states: "3bQYQbOOO#hQcO'#C{O$eOSO'#C}O$sQbO'#E]OOQ`'#DW'#DWOOQa'#DT'#DTO%vQbO'#D]O&{QcO'#EQOOQa'#EQ'#EQO)PQcO'#EPO)xQRO'#C|O*]QcO'#D{O*tQcO'#D{O+VQbO'#CzO+}OpO'#CxOOQ`'#D|'#D|O,SQbO'#D{O,bQbO'#EcOOQ`'#Db'#DbO-VQRO'#DjOOQ`'#D{'#D{O-kQQO'#DzOOQ`'#Dz'#DzOOQ`'#Dl'#DlQYQbOOO-sQbO'#DUOOQa'#EP'#EPOOQ`'#D`'#D`OOQ`'#Eb'#EbOOQ`'#Ds'#DsO-}QbO,59cO.bQbO'#DPO.jQWO'#DQOOOO'#ES'#ESOOOO'#Dm'#DmO/OOSO,59iOOQa,59i,59iOOQ`'#Do'#DoO/^QbO'#DXO/fQQO,5:wOOQ`'#Dn'#DnO/kQbO,59wO/rQQO,59oOOQa,59w,59wO/}QbO,59wO,bQbO,59hO,bQbO,59hO,bQbO,59hO,bQbO,59yO,bQbO,59yO,bQbO,59yO0XQRO,59fO0`QRO,59fO0qQRO,59fO0lQQO,59fO0|QQO,59fO1UObO,59dO1aQbO'#DtO1lQbO,59bO1}QRO,5:}O2UQRO,5:}O2aQbO,5:UO2aQbO,5:VOOQ`,5:f,5:fOOQ`-E7j-E7jOOQ`,59p,59pOOQ`-E7q-E7qOOOO,59k,59kOOOO,59l,59lOOOO-E7k-E7kOOQa1G/T1G/TOOQ`-E7m-E7mO2kQbO1G0cOOQ`-E7l-E7lO2xQQO1G/ZOOQa1G/c1G/cO3TQbO1G/cOOQO'#Dq'#DqO2xQQO1G/ZOOQa1G/Z1G/ZOOQ`'#Dr'#DrO3TQbO1G/cOOQa1G/S1G/SO3vQcO1G/SO4QQcO1G/SO4[QcO1G/SOOQa1G/e1G/eO5nQcO1G/eO5uQcO1G/eO5|QcO1G/eOOQa1G/Q1G/QOOQa1G/O1G/OO!ZQbO'#C{O%}QbO'#CwOOQ`,5:`,5:`OOQ`-E7r-E7rO6TQbO1G0iOOQ`1G/p1G/pOOQ`1G/q1G/qO6bQbO7+%}O6gQbO7+&OO6wQQO7+$uOOQa7+$u7+$uO7SQbO7+$}OOQa7+$}7+$}OOQO-E7o-E7oOOQ`-E7p-E7pOOQ`'#Dd'#DdO7^QbO7+&TO7cQbO7+&UOOQ`<RQbO<WQbO<`QbO<kQQO,59uO>pQbO,59xOOQ`<h#i#o7^#o#p#{#p#q@`#q;'S#{;'S;=`$d<%l~#{~O#{~~@yS$QUrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUrS!lYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UrS#OQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZrS!mYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!mYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O!w~~'aO!u~U'hUrS!rQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUrS#TQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWrSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZYrSmQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OWrSOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oWrSmQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^WrSOt#{uw#{x!P#{!P!Q+v!Q#O#{#P;'S#{;'S;=`$d<%lO#{U+{^rSOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q#{!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wU-O^rSvQOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q0o!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wQ.PXvQOY-zZ!P-z!P!Q.l!Q!}-z!}#O/Z#O#P0Y#P;'S-z;'S;=`0i<%lO-zQ.oP!P!Q.rQ.wUvQ#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-zU0tWrSOt#{uw#{x!P#{!P!Q1^!Q#O#{#P;'S#{;'S;=`$d<%lO#{U1ebrSvQOt#{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[rSOY2mYZ#{Zt2mtu/Zuw2mwx/Zx#O2m#O#P/s#P#Q,w#Q;'S2m;'S;=`3h<%lO2mU3kP;=`<%l2mU3qP;=`<%l,wU3{UrS|QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4fW!}QrSOt#{uw#{x!_#{!_!`5O!`#O#{#P;'S#{;'S;=`$d<%lO#{U5TVrSOt#{uw#{x#O#{#P#Q5j#Q;'S#{;'S;=`$d<%lO#{U5qU!|QrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6YO!x~U6aU#SQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6zUrS!OQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7cYrSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{U8YUyQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U8qZrSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#U9d#U#o7^#o;'S#{;'S;=`$d<%lO#{U9i[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#`7^#`#a:_#a#o7^#o;'S#{;'S;=`$d<%lO#{U:d[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#g7^#g#h;Y#h#o7^#o;'S#{;'S;=`$d<%lO#{U;_[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#X7^#X#Yo[!zWrSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#f7^#f#g?e#g#o7^#o;'S#{;'S;=`$d<%lO#{U?j[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#i7^#i#j;Y#j#o7^#o;'S#{;'S;=`$d<%lO#{U@gU!TQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~AOO#X~", - tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!q~~", 11)], + 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#ch#i#o7^#o#p#{#p#q@`#q;'S#{;'S;=`$d<%l~#{~O#{~~@yS$QUrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUrS!uYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UrS#XQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZrS!vYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!vYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O#Q~~'aO#O~U'hUrS!{QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUrS#^QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWrSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZYrSmQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OWrSOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oWrSmQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^WrSOt#{uw#{x!P#{!P!Q+v!Q#O#{#P;'S#{;'S;=`$d<%lO#{U+{^rSOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q#{!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wU-O^rSvQOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q0o!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wQ.PXvQOY-zZ!P-z!P!Q.l!Q!}-z!}#O/Z#O#P0Y#P;'S-z;'S;=`0i<%lO-zQ.oP!P!Q.rQ.wUvQ#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-zU0tWrSOt#{uw#{x!P#{!P!Q1^!Q#O#{#P;'S#{;'S;=`$d<%lO#{U1ebrSvQOt#{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[rSOY2mYZ#{Zt2mtu/Zuw2mwx/Zx#O2m#O#P/s#P#Q,w#Q;'S2m;'S;=`3h<%lO2mU3kP;=`<%l2mU3qP;=`<%l,wU3{UrS|QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4fW#WQrSOt#{uw#{x!_#{!_!`5O!`#O#{#P;'S#{;'S;=`$d<%lO#{U5TVrSOt#{uw#{x#O#{#P#Q5j#Q;'S#{;'S;=`$d<%lO#{U5qU#VQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6YO#R~U6aU#]QrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6zUrS!TQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7cYrSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{U8YUyQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U8qZrSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#U9d#U#o7^#o;'S#{;'S;=`$d<%lO#{U9i[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#`7^#`#a:_#a#o7^#o;'S#{;'S;=`$d<%lO#{U:d[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#g7^#g#h;Y#h#o7^#o;'S#{;'S;=`$d<%lO#{U;_[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#X7^#X#Yo[#TWrSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#f7^#f#g?e#g#o7^#o;'S#{;'S;=`$d<%lO#{U?j[rSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#i7^#i#j;Y#j#o7^#o;'S#{;'S;=`$d<%lO#{U@gU!YQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~AOO#d~", + tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!z~~", 11)], topRules: {"Program":[0,25]}, 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: 1150 + tokenPrec: 1562 }) -- 2.50.1