diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 4e35747..6934f06 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -9,6 +9,7 @@ import { checkTreeForErrors, getAllChildren, getAssignmentParts, + getCompoundAssignmentParts, getBinaryParts, getDotGetParts, getFunctionCallParts, @@ -247,6 +248,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 c5fb786..96f6bee 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, ...rest] = 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 80cdef5..9da24da 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 } @@ -55,6 +55,7 @@ consumeToTerminator { Throw | IfExpr | FunctionDef | + CompoundAssign | Assign | BinOp | ConditionalOp | @@ -181,6 +182,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 8a80f73..ab4011b 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -14,45 +14,51 @@ 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, - CatchExpr = 40, - keyword = 63, - TryBlock = 42, - FinallyExpr = 43, - Underscore = 46, - Array = 47, - Null = 48, - ConditionalOp = 49, - PositionalArg = 50, - TryExpr = 52, - Throw = 54, - IfExpr = 56, - SingleLineThenBlock = 58, - ThenBlock = 59, - ElseIfExpr = 60, - ElseExpr = 62, - Assign = 64 + 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, + 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 ec84f41..172f6a7 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,catch:82, finally:88, end:90, null:96, try:106, throw:110, if:114, elseif:122, else:126} +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: "8xQYQbOOO#tQcO'#CvO$qOSO'#CxO%PQbO'#E`OOQ`'#DR'#DROOQa'#DO'#DOO&SQbO'#D]O'XQcO'#ETOOQa'#ET'#ETO)cQcO'#ESO)vQRO'#CwO+SQcO'#EOO+dQcO'#EOO+nQbO'#CuO,fOpO'#CsOOQ`'#EP'#EPO,kQbO'#EOO,rQQO'#EfOOQ`'#Db'#DbO,wQbO'#DdO,wQbO'#EhOOQ`'#Df'#DfO-lQRO'#DnOOQ`'#EO'#EOO-qQQO'#D}OOQ`'#D}'#D}OOQ`'#Do'#DoQYQbOOO-yQbO'#DPOOQa'#ES'#ESOOQ`'#D`'#D`OOQ`'#Ee'#EeOOQ`'#Dv'#DvO.TQbO,59^O.nQbO'#CzO.vQWO'#C{OOOO'#EV'#EVOOOO'#Dp'#DpO/[OSO,59dOOQa,59d,59dOOQ`'#Dr'#DrO/jQbO'#DSO/rQQO,5:zOOQ`'#Dq'#DqO/wQbO,59wO0OQQO,59jOOQa,59w,59wO0ZQbO,59wO,wQbO,59cO,wQbO,59cO,wQbO,59cO,wQbO,59yO,wQbO,59yO,wQbO,59yO0eQRO,59aO0lQRO,59aO0}QRO,59aO0xQQO,59aO1YQQO,59aO1bObO,59_O1mQbO'#DwO1xQbO,59]O2aQbO,5;QO2tQcO,5:OO3jQcO,5:OO3zQcO,5:OO4pQRO,5;SO4wQRO,5;SO5SQbO,5:YOOQ`,5:i,5:iOOQ`-E7m-E7mOOQ`,59k,59kOOQ`-E7t-E7tOOOO,59f,59fOOOO,59g,59gOOOO-E7n-E7nOOQa1G/O1G/OOOQ`-E7p-E7pO5dQbO1G0fOOQ`-E7o-E7oO5wQQO1G/UOOQa1G/c1G/cO6SQbO1G/cOOQO'#Dt'#DtO5wQQO1G/UOOQa1G/U1G/UOOQ`'#Du'#DuO6SQbO1G/cOOQa1G.}1G.}O6{QcO1G.}O7VQcO1G.}O7aQcO1G.}OOQa1G/e1G/eO9PQcO1G/eO9WQcO1G/eO9_QcO1G/eOOQa1G.{1G.{OOQa1G.y1G.yO!aQbO'#CvO&ZQbO'#CrOOQ`,5:c,5:cOOQ`-E7u-E7uO9fQbO1G0lO9qQbO1G0mO:_QbO1G0nOOQ`1G/t1G/tO:rQbO7+&QO9qQbO7+&SO:}QQO7+$pOOQa7+$p7+$pO;YQbO7+$}OOQa7+$}7+$}OOQO-E7r-E7rOOQ`-E7s-E7sO;dQbO'#DUO;iQQO'#DXOOQ`7+&W7+&WO;nQbO7+&WO;sQbO7+&WOOQ`'#Ds'#DsO;{QQO'#DsOPQbO<[QQO,59pO>aQbO,59sOOQ`<tQbO<yQbO<RQbO<WQbO<`QbO<kQQO,59uO>pQbO,59xOOQ`<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!oYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UmS#RQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZmS!pYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!pYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O!z~~'aO!x~U'hUmS!uQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUmS#WQOt#{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#QQmSOt#{uw#{x!_#{!_!`5O!`#O#{#P;'S#{;'S;=`$d<%lO#{U5TVmSOt#{uw#{x#O#{#P#Q5j#Q;'S#{;'S;=`$d<%lO#{U5qU#PQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6YO!{~U6aU#VQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6zUmS!OQOt#{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[!}WmSOt#{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!TQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~AOO#^~", - tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!t~~", 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: 1547 + 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: 1562 }) 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