summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--chapter17/README.md9
-rw-r--r--chapter17/src/main/java/com/seaofnodes/simple/Parser.java26
-rw-r--r--chapter17/src/main/java/com/seaofnodes/simple/node/ShrNode.java5
-rw-r--r--chapter17/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java77
-rw-r--r--chapter18/src/main/java/com/seaofnodes/simple/Parser.java22
-rw-r--r--chapter18/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java94
-rw-r--r--chapter19/src/main/java/com/seaofnodes/simple/Parser.java22
-rw-r--r--chapter19/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java94
-rw-r--r--chapter20/src/main/java/com/seaofnodes/simple/Parser.java22
-rw-r--r--chapter20/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java94
-rw-r--r--chapter21/src/main/java/com/seaofnodes/simple/Parser.java22
-rw-r--r--chapter21/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java94
-rw-r--r--chapter22/src/main/java/com/seaofnodes/simple/Parser.java62
-rw-r--r--chapter22/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java94
-rw-r--r--chapter22/src/test/java/com/seaofnodes/simple/LiteralTest.java65
-rw-r--r--chapter23/src/main/java/com/seaofnodes/simple/Parser.java64
-rw-r--r--chapter23/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java2
-rw-r--r--chapter23/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java94
-rw-r--r--chapter23/src/test/java/com/seaofnodes/simple/LiteralTest.java65
-rw-r--r--chapter24/src/main/java/com/seaofnodes/simple/Parser.java64
-rw-r--r--chapter24/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java94
-rw-r--r--chapter24/src/test/java/com/seaofnodes/simple/LiteralTest.java65
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/Parser.java26
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java94
-rw-r--r--docs/chapter-backports.md3
25 files changed, 1300 insertions, 73 deletions
diff --git a/chapter17/README.md b/chapter17/README.md
index 331cdc3d..cdbca21b 100644
--- a/chapter17/README.md
+++ b/chapter17/README.md
@@ -25,7 +25,12 @@ Allow pre-increment for identifiers only, for now: `--pc`.
## Operator assignment
Along the same lines as post-increment, we now allow `op=` assignment
-with semantics similar to other languages.
+on local variables with semantics similar to other languages. The supported
+operators are `+=`, `-=`, `*=`, `/=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, and `>>>=`.
+Bitwise and shift assignments require integer operands; `>>=` preserves the sign
+while `>>>=` shifts in zeros. The assignment evaluates its right-hand side once,
+stores the result narrowed to the variable's type, and yields that stored value.
+Assignments associate right-to-left, as in `x |= y <<= 2`.
`x += y`
@@ -159,4 +164,4 @@ for( int i=0; i<ary#; i++ )
if( ary[i]==e )
return i;
return -1;
-``` \ No newline at end of file
+```
diff --git a/chapter17/src/main/java/com/seaofnodes/simple/Parser.java b/chapter17/src/main/java/com/seaofnodes/simple/Parser.java
index adc47edc..789eea19 100644
--- a/chapter17/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/chapter17/src/main/java/com/seaofnodes/simple/Parser.java
@@ -916,13 +916,19 @@ public class Parser {
if( n._final )
throw error("Cannot reassign final '"+n._name+"'");
Node op = switch(ch) {
- case '+' -> new AddNode(rvalue,null);
- case '-' -> new SubNode(rvalue,null);
- case '*' -> new MulNode(rvalue,null);
- case '/' -> new DivNode(rvalue,null);
- case 1 -> new AddNode(rvalue,con( 1));
- case (char)-1 -> new AddNode(rvalue,con(-1));
- default -> throw Utils.TODO();
+ case '+' -> new AddNode(rvalue,null);
+ case '-' -> new SubNode(rvalue,null);
+ case '*' -> new MulNode(rvalue,null);
+ case '/' -> new DivNode(rvalue,null);
+ case '&' -> new AndNode(rvalue,null);
+ case '|' -> new OrNode(rvalue,null);
+ case '^' -> new XorNode(rvalue,null);
+ case Lexer.SHL -> new ShlNode(rvalue,null);
+ case Lexer.SAR -> new SarNode(rvalue,null);
+ case Lexer.SHR -> new ShrNode(rvalue,null);
+ case 1 -> new AddNode(rvalue,con( 1));
+ case (char)-1 -> new AddNode(rvalue,con(-1));
+ default -> throw Utils.TODO();
};
// Return pre-value (x+=1) or post-value (x++)
boolean pre = op.in(2)==null;
@@ -1403,8 +1409,14 @@ public class Parser {
// Next oper= character, or 0.
// As a convenience, mark "++" as a char 1 and "--" as char -1 (65535)
// Disallow e.g. "arg--1" which should parse as "arg - -1"
+ // Distinct tags for the multi-character compound operators.
+ static final char SHL = 2, SAR = 3, SHR = 4;
+
public char matchOperAssign() {
skipWhiteSpace();
+ if( match("<<=" ) ) return SHL;
+ if( match(">>>=") ) return SHR;
+ if( match(">>=" ) ) return SAR;
if( _position+2 >= _input.length ) return 0;
char ch0 = (char)_input[_position];
if( "+-/*&|^".indexOf(ch0) == -1 ) return 0;
diff --git a/chapter17/src/main/java/com/seaofnodes/simple/node/ShrNode.java b/chapter17/src/main/java/com/seaofnodes/simple/node/ShrNode.java
index 22eb45a0..779193de 100644
--- a/chapter17/src/main/java/com/seaofnodes/simple/node/ShrNode.java
+++ b/chapter17/src/main/java/com/seaofnodes/simple/node/ShrNode.java
@@ -49,4 +49,9 @@ public class ShrNode extends Node {
return null;
}
@Override Node copy(Node lhs, Node rhs) { return new ShrNode(lhs,rhs); }
+ @Override String err() {
+ if( !(in(1)._type instanceof TypeInteger) ) return "Cannot '>>>' " + in(1)._type;
+ if( !(in(2)._type instanceof TypeInteger) ) return "Cannot '>>>' " + in(2)._type;
+ return null;
+ }
}
diff --git a/chapter17/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/chapter17/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
new file mode 100644
index 00000000..0fa4283b
--- /dev/null
+++ b/chapter17/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -0,0 +1,77 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.evaluator.Evaluator;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class CompoundAssignmentTest {
+ private static final String[] OPS = {"&=", "|=", "^=", "<<=", ">>=", ">>>="};
+
+ private static long apply(String op, long lhs, long rhs) {
+ return switch (op) {
+ case "&=" -> lhs & rhs;
+ case "|=" -> lhs | rhs;
+ case "^=" -> lhs ^ rhs;
+ case "<<=" -> lhs << rhs;
+ case ">>=" -> lhs >> rhs;
+ case ">>>=" -> lhs >>> rhs;
+ default -> throw new AssertionError(op);
+ };
+ }
+
+ @Test public void testLocals() {
+ for (String op : OPS)
+ for (long arg : new long[]{-8, 0, 5, 64}) {
+ long expected = apply(op, arg, 3);
+ check("int x=arg; int r=(x " + op + " 3); return x*10+r;",
+ arg, expected*11);
+ }
+ }
+
+ @Test public void testRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int x=arg; int count=0; int r=(x " + op +
+ " (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11);
+ }
+ }
+
+ @Test public void testNarrowingAndChaining() {
+ check("u8 x=255; x <<= 1; return x;", 0, 254);
+ check("u8 x=255; x ^= 256; return x;", 0, 255);
+ check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312);
+ check("int x=-8; x >>= 65; return x;", 0, -4);
+ check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3);
+ }
+
+ @Test public void testExistingOperators() {
+ check("int x=8; x+=2; x-=1; x*=3; x/=9; return x;", 0, 3);
+ check("int x=arg; int y=x++; int z=x--; return x*100+y*10+z;", 5, 556);
+ check("return (arg << 2) + (arg >> 1) + (arg >>> 1);", 8, 40);
+ check("return arg <= 8;", 8, 1);
+ check("return arg >= 8;", 8, 1);
+ }
+
+ @Test public void testRejectedAssignments() {
+ for (String op : OPS) {
+ rejected("val x=3; x " + op + " 1; return x;");
+ rejected("int x=3; x " + op + " 1.5; return x;");
+ }
+ }
+
+ private static void rejected(String src) {
+ try {
+ check(src, 0, 0);
+ fail("Accepted invalid assignment: " + src);
+ } catch (RuntimeException e) {
+ assertEquals("Must report a language error", RuntimeException.class, e.getClass());
+ assertNotNull(e.getMessage());
+ assertFalse("Must report a language error: " + e, e.getMessage().contains("Not yet implemented"));
+ }
+ }
+
+ private static void check(String src, long arg, long expected) {
+ var stop = new Parser(src).parse().iterate();
+ assertEquals(src, Long.valueOf(expected), Evaluator.evaluate(stop, arg));
+ }
+}
diff --git a/chapter18/src/main/java/com/seaofnodes/simple/Parser.java b/chapter18/src/main/java/com/seaofnodes/simple/Parser.java
index ad617b2b..24f0806a 100644
--- a/chapter18/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/chapter18/src/main/java/com/seaofnodes/simple/Parser.java
@@ -1107,11 +1107,17 @@ public class Parser {
// flt + flt ==>> use float op
Node op = switch(ch) {
case 1, (char)-1,
- '+' -> new AddNode(lhs,rhs);
- case '-' -> new SubNode(lhs,rhs);
- case '*' -> new MulNode(lhs,rhs);
- case '/' -> new DivNode(lhs,rhs);
- default -> throw Utils.TODO();
+ '+' -> new AddNode(lhs,rhs);
+ case '-' -> new SubNode(lhs,rhs);
+ case '*' -> new MulNode(lhs,rhs);
+ case '/' -> new DivNode(lhs,rhs);
+ case '&' -> new AndNode(loc(),lhs,rhs);
+ case '|' -> new OrNode(loc(),lhs,rhs);
+ case '^' -> new XorNode(loc(),lhs,rhs);
+ case Lexer.SHL -> new ShlNode(loc(),lhs,rhs);
+ case Lexer.SAR -> new SarNode(loc(),lhs,rhs);
+ case Lexer.SHR -> new ShrNode(loc(),lhs,rhs);
+ default -> throw Utils.TODO();
};
// Convert to float ops, or narrow int types; error if not declared type.
// Also, if postfix LHS is still keep()
@@ -1700,8 +1706,14 @@ public class Parser {
// Next oper= character, or 0.
// As a convenience, mark "++" as a char 1 and "--" as char -1 (65535)
+ // Distinct tags for the multi-character compound operators.
+ static final char SHL = 2, SAR = 3, SHR = 4;
+
public char matchOperAssign() {
skipWhiteSpace();
+ if( match("<<=" ) ) return SHL;
+ if( match(">>>=") ) return SHR;
+ if( match(">>=" ) ) return SAR;
if( _position+2 >= _input.length ) return 0;
char ch0 = (char)_input[_position];
if( "+-/*&|^".indexOf(ch0) == -1 ) return 0;
diff --git a/chapter18/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/chapter18/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
new file mode 100644
index 00000000..f78e1b21
--- /dev/null
+++ b/chapter18/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class CompoundAssignmentTest {
+ private static final String[] OPS = {"&=", "|=", "^=", "<<=", ">>=", ">>>="};
+
+ private static long apply(String op, long lhs, long rhs) {
+ return switch (op) {
+ case "&=" -> lhs & rhs;
+ case "|=" -> lhs | rhs;
+ case "^=" -> lhs ^ rhs;
+ case "<<=" -> lhs << rhs;
+ case ">>=" -> lhs >> rhs;
+ case ">>>=" -> lhs >>> rhs;
+ default -> throw new AssertionError(op);
+ };
+ }
+
+ @Test public void testLocals() {
+ for (String op : OPS)
+ for (long arg : new long[]{-8, 0, 5, 64}) {
+ long expected = apply(op, arg, 3);
+ check("int x=arg; int r=(x " + op + " 3); return x*10+r;",
+ arg, expected*11);
+ }
+ }
+
+ @Test public void testRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int x=arg; int count=0; int r=(x " + op +
+ " (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11);
+ }
+ }
+
+ @Test public void testNarrowingAndChaining() {
+ check("u8 x=255; x <<= 1; return x;", 0, 254);
+ check("u8 x=255; x ^= 256; return x;", 0, 255);
+ check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312);
+ check("int x=-8; x >>= 65; return x;", 0, -4);
+ check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3);
+ }
+
+ @Test public void testExistingOperators() {
+ check("int x=8; x+=2; x-=1; x*=3; x/=9; return x;", 0, 3);
+ check("int x=arg; int y=x++; int z=x--; return x*100+y*10+z;", 5, 556);
+ check("return (arg << 2) + (arg >> 1) + (arg >>> 1);", 8, 40);
+ check("return arg <= 8;", 8, 1);
+ check("return arg >= 8;", 8, 1);
+ }
+
+ @Test public void testRejectedAssignments() {
+ for (String op : OPS) {
+ rejected("val x=3; x " + op + " 1; return x;");
+ rejected("int x=3; x " + op + " 1.5; return x;");
+ }
+ }
+
+ private static void rejected(String src) {
+ try {
+ check(src, 0, 0);
+ fail("Accepted invalid assignment: " + src);
+ } catch (RuntimeException e) {
+ assertTrue("Must report a language error: " + e, e instanceof Parser.ParseException);
+ assertNotNull(e.getMessage());
+ assertFalse("Must report a language error: " + e, e.getMessage().contains("Not yet implemented"));
+ }
+ }
+
+ @Test public void testFields() {
+ for (String op : OPS) {
+ long expected = apply(op, -8, 3);
+ check("struct S { int x; }; S !s=new S; s.x=arg; int r=(s.x " +
+ op + " 3); return s.x*10+r;", -8, expected*11);
+ }
+ }
+
+ @Test public void testArrayAddressAndRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int[] !a=new int[2]; a[0]=arg; int i=0; int count=0; " +
+ "int r=(a[i++] " + op + " (count=count+1)); " +
+ "return i*1000+count*100+a[0]*10+r;", 5, 1100+expected*11);
+ }
+ }
+
+ private static void check(String src, long arg, long expected) {
+ var code = new CodeGen(src).parse().opto().typeCheck();
+ assertEquals(src, Long.toString(expected), Eval2.eval(code, arg));
+ }
+}
diff --git a/chapter19/src/main/java/com/seaofnodes/simple/Parser.java b/chapter19/src/main/java/com/seaofnodes/simple/Parser.java
index 19822738..053b8c66 100644
--- a/chapter19/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/chapter19/src/main/java/com/seaofnodes/simple/Parser.java
@@ -1089,11 +1089,17 @@ public class Parser {
// flt + flt ==>> use float op
Node op = switch(ch) {
case 1, (char)-1,
- '+' -> new AddNode(lhs,rhs);
- case '-' -> new SubNode(lhs,rhs);
- case '*' -> new MulNode(lhs,rhs);
- case '/' -> new DivNode(lhs,rhs);
- default -> throw Utils.TODO();
+ '+' -> new AddNode(lhs,rhs);
+ case '-' -> new SubNode(lhs,rhs);
+ case '*' -> new MulNode(lhs,rhs);
+ case '/' -> new DivNode(lhs,rhs);
+ case '&' -> new AndNode(loc(),lhs,rhs);
+ case '|' -> new OrNode(loc(),lhs,rhs);
+ case '^' -> new XorNode(loc(),lhs,rhs);
+ case Lexer.SHL -> new ShlNode(loc(),lhs,rhs);
+ case Lexer.SAR -> new SarNode(loc(),lhs,rhs);
+ case Lexer.SHR -> new ShrNode(loc(),lhs,rhs);
+ default -> throw Utils.TODO();
};
// Convert to float ops, or narrow int types; error if not declared type.
// Also, if postfix LHS is still keep()
@@ -1699,8 +1705,14 @@ public class Parser {
// Next oper= character, or 0.
// As a convenience, mark "++" as a char 1 and "--" as char -1 (65535)
+ // Distinct tags for the multi-character compound operators.
+ static final char SHL = 2, SAR = 3, SHR = 4;
+
public char matchOperAssign() {
skipWhiteSpace();
+ if( match("<<=" ) ) return SHL;
+ if( match(">>>=") ) return SHR;
+ if( match(">>=" ) ) return SAR;
if( _position+2 >= _input.length ) return 0;
char ch0 = (char)_input[_position];
if( "+-/*&|^".indexOf(ch0) == -1 ) return 0;
diff --git a/chapter19/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/chapter19/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
new file mode 100644
index 00000000..f78e1b21
--- /dev/null
+++ b/chapter19/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class CompoundAssignmentTest {
+ private static final String[] OPS = {"&=", "|=", "^=", "<<=", ">>=", ">>>="};
+
+ private static long apply(String op, long lhs, long rhs) {
+ return switch (op) {
+ case "&=" -> lhs & rhs;
+ case "|=" -> lhs | rhs;
+ case "^=" -> lhs ^ rhs;
+ case "<<=" -> lhs << rhs;
+ case ">>=" -> lhs >> rhs;
+ case ">>>=" -> lhs >>> rhs;
+ default -> throw new AssertionError(op);
+ };
+ }
+
+ @Test public void testLocals() {
+ for (String op : OPS)
+ for (long arg : new long[]{-8, 0, 5, 64}) {
+ long expected = apply(op, arg, 3);
+ check("int x=arg; int r=(x " + op + " 3); return x*10+r;",
+ arg, expected*11);
+ }
+ }
+
+ @Test public void testRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int x=arg; int count=0; int r=(x " + op +
+ " (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11);
+ }
+ }
+
+ @Test public void testNarrowingAndChaining() {
+ check("u8 x=255; x <<= 1; return x;", 0, 254);
+ check("u8 x=255; x ^= 256; return x;", 0, 255);
+ check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312);
+ check("int x=-8; x >>= 65; return x;", 0, -4);
+ check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3);
+ }
+
+ @Test public void testExistingOperators() {
+ check("int x=8; x+=2; x-=1; x*=3; x/=9; return x;", 0, 3);
+ check("int x=arg; int y=x++; int z=x--; return x*100+y*10+z;", 5, 556);
+ check("return (arg << 2) + (arg >> 1) + (arg >>> 1);", 8, 40);
+ check("return arg <= 8;", 8, 1);
+ check("return arg >= 8;", 8, 1);
+ }
+
+ @Test public void testRejectedAssignments() {
+ for (String op : OPS) {
+ rejected("val x=3; x " + op + " 1; return x;");
+ rejected("int x=3; x " + op + " 1.5; return x;");
+ }
+ }
+
+ private static void rejected(String src) {
+ try {
+ check(src, 0, 0);
+ fail("Accepted invalid assignment: " + src);
+ } catch (RuntimeException e) {
+ assertTrue("Must report a language error: " + e, e instanceof Parser.ParseException);
+ assertNotNull(e.getMessage());
+ assertFalse("Must report a language error: " + e, e.getMessage().contains("Not yet implemented"));
+ }
+ }
+
+ @Test public void testFields() {
+ for (String op : OPS) {
+ long expected = apply(op, -8, 3);
+ check("struct S { int x; }; S !s=new S; s.x=arg; int r=(s.x " +
+ op + " 3); return s.x*10+r;", -8, expected*11);
+ }
+ }
+
+ @Test public void testArrayAddressAndRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int[] !a=new int[2]; a[0]=arg; int i=0; int count=0; " +
+ "int r=(a[i++] " + op + " (count=count+1)); " +
+ "return i*1000+count*100+a[0]*10+r;", 5, 1100+expected*11);
+ }
+ }
+
+ private static void check(String src, long arg, long expected) {
+ var code = new CodeGen(src).parse().opto().typeCheck();
+ assertEquals(src, Long.toString(expected), Eval2.eval(code, arg));
+ }
+}
diff --git a/chapter20/src/main/java/com/seaofnodes/simple/Parser.java b/chapter20/src/main/java/com/seaofnodes/simple/Parser.java
index 07f82625..343a3d47 100644
--- a/chapter20/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/chapter20/src/main/java/com/seaofnodes/simple/Parser.java
@@ -1091,11 +1091,17 @@ public class Parser {
// flt + flt ==>> use float op
Node op = switch(ch) {
case 1, (char)-1,
- '+' -> new AddNode(lhs,rhs);
- case '-' -> new SubNode(lhs,rhs);
- case '*' -> new MulNode(lhs,rhs);
- case '/' -> new DivNode(lhs,rhs);
- default -> throw Utils.TODO();
+ '+' -> new AddNode(lhs,rhs);
+ case '-' -> new SubNode(lhs,rhs);
+ case '*' -> new MulNode(lhs,rhs);
+ case '/' -> new DivNode(lhs,rhs);
+ case '&' -> new AndNode(loc(),lhs,rhs);
+ case '|' -> new OrNode(loc(),lhs,rhs);
+ case '^' -> new XorNode(loc(),lhs,rhs);
+ case Lexer.SHL -> new ShlNode(loc(),lhs,rhs);
+ case Lexer.SAR -> new SarNode(loc(),lhs,rhs);
+ case Lexer.SHR -> new ShrNode(loc(),lhs,rhs);
+ default -> throw Utils.TODO();
};
// Convert to float ops, or narrow int types; error if not declared type.
// Also, if postfix LHS is still keep()
@@ -1701,8 +1707,14 @@ public class Parser {
// Next oper= character, or 0.
// As a convenience, mark "++" as a char 1 and "--" as char -1 (65535)
+ // Distinct tags for the multi-character compound operators.
+ static final char SHL = 2, SAR = 3, SHR = 4;
+
public char matchOperAssign() {
skipWhiteSpace();
+ if( match("<<=" ) ) return SHL;
+ if( match(">>>=") ) return SHR;
+ if( match(">>=" ) ) return SAR;
if( _position+2 >= _input.length ) return 0;
char ch0 = (char)_input[_position];
if( "+-/*&|^".indexOf(ch0) == -1 ) return 0;
diff --git a/chapter20/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/chapter20/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
new file mode 100644
index 00000000..0d67d3c5
--- /dev/null
+++ b/chapter20/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class CompoundAssignmentTest {
+ private static final String[] OPS = {"&=", "|=", "^=", "<<=", ">>=", ">>>="};
+
+ private static long apply(String op, long lhs, long rhs) {
+ return switch (op) {
+ case "&=" -> lhs & rhs;
+ case "|=" -> lhs | rhs;
+ case "^=" -> lhs ^ rhs;
+ case "<<=" -> lhs << rhs;
+ case ">>=" -> lhs >> rhs;
+ case ">>>=" -> lhs >>> rhs;
+ default -> throw new AssertionError(op);
+ };
+ }
+
+ @Test public void testLocals() {
+ for (String op : OPS)
+ for (long arg : new long[]{-8, 0, 5, 64}) {
+ long expected = apply(op, arg, 3);
+ check("int x=arg; int r=(x " + op + " 3); return x*10+r;",
+ arg, expected*11);
+ }
+ }
+
+ @Test public void testRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int x=arg; int count=0; int r=(x " + op +
+ " (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11);
+ }
+ }
+
+ @Test public void testNarrowingAndChaining() {
+ check("u8 x=255; x <<= 1; return x;", 0, 254);
+ check("u8 x=255; x ^= 256; return x;", 0, 255);
+ check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312);
+ check("int x=-8; x >>= 65; return x;", 0, -4);
+ check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3);
+ }
+
+ @Test public void testExistingOperators() {
+ check("int x=8; x+=2; x-=1; x*=3; x/=9; return x;", 0, 3);
+ check("int x=arg; int y=x++; int z=x--; return x*100+y*10+z;", 5, 556);
+ check("return (arg << 2) + (arg >> 1) + (arg >>> 1);", 8, 40);
+ check("return arg <= 8;", 8, 1);
+ check("return arg >= 8;", 8, 1);
+ }
+
+ @Test public void testRejectedAssignments() {
+ for (String op : OPS) {
+ rejected("val x=3; x " + op + " 1; return x;");
+ rejected("int x=3; x " + op + " 1.5; return x;");
+ }
+ }
+
+ private static void rejected(String src) {
+ try {
+ check(src, 0, 0);
+ fail("Accepted invalid assignment: " + src);
+ } catch (RuntimeException e) {
+ assertTrue("Must report a language error: " + e, e instanceof Parser.ParseException);
+ assertNotNull(e.getMessage());
+ assertFalse("Must report a language error: " + e, e.getMessage().contains("Not yet implemented"));
+ }
+ }
+
+ @Test public void testFields() {
+ for (String op : OPS) {
+ long expected = apply(op, -8, 3);
+ check("struct S { int x; }; S !s=new S; s.x=arg; int r=(s.x " +
+ op + " 3); return s.x*10+r;", -8, expected*11);
+ }
+ }
+
+ @Test public void testArrayAddressAndRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int[] !a=new int[2]; a[0]=arg; int i=0; int count=0; " +
+ "int r=(a[i++] " + op + " (count=count+1)); " +
+ "return i*1000+count*100+a[0]*10+r;", 5, 1100+expected*11);
+ }
+ }
+
+ private static void check(String src, long arg, long expected) {
+ var code = new CodeGen(src).parse().opto().typeCheck();
+ assertEquals(src, Long.toString(expected), Eval2.eval(code, arg));
+ }
+}
diff --git a/chapter21/src/main/java/com/seaofnodes/simple/Parser.java b/chapter21/src/main/java/com/seaofnodes/simple/Parser.java
index 219c3c44..8e67ed10 100644
--- a/chapter21/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/chapter21/src/main/java/com/seaofnodes/simple/Parser.java
@@ -1109,11 +1109,17 @@ public class Parser {
// flt + flt ==>> use float op
Node op = switch(ch) {
case 1, (char)-1,
- '+' -> new AddNode(lhs,rhs);
- case '-' -> new SubNode(lhs,rhs);
- case '*' -> new MulNode(lhs,rhs);
- case '/' -> new DivNode(lhs,rhs);
- default -> throw Utils.TODO();
+ '+' -> new AddNode(lhs,rhs);
+ case '-' -> new SubNode(lhs,rhs);
+ case '*' -> new MulNode(lhs,rhs);
+ case '/' -> new DivNode(lhs,rhs);
+ case '&' -> new AndNode(loc(),lhs,rhs);
+ case '|' -> new OrNode(loc(),lhs,rhs);
+ case '^' -> new XorNode(loc(),lhs,rhs);
+ case Lexer.SHL -> new ShlNode(loc(),lhs,rhs);
+ case Lexer.SAR -> new SarNode(loc(),lhs,rhs);
+ case Lexer.SHR -> new ShrNode(loc(),lhs,rhs);
+ default -> throw Utils.TODO();
};
// Convert to float ops, or narrow int types; error if not declared type.
// Also, if postfix LHS is still keep()
@@ -1721,8 +1727,14 @@ public class Parser {
// Next oper= character, or 0.
// As a convenience, mark "++" as a char 1 and "--" as char -1 (65535)
+ // Distinct tags for the multi-character compound operators.
+ static final char SHL = 2, SAR = 3, SHR = 4;
+
public char matchOperAssign() {
skipWhiteSpace();
+ if( match("<<=" ) ) return SHL;
+ if( match(">>>=") ) return SHR;
+ if( match(">>=" ) ) return SAR;
if( _position+2 >= _input.length ) return 0;
char ch0 = (char)_input[_position];
if( "+-/*&|^".indexOf(ch0) == -1 ) return 0;
diff --git a/chapter21/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/chapter21/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
new file mode 100644
index 00000000..0d67d3c5
--- /dev/null
+++ b/chapter21/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class CompoundAssignmentTest {
+ private static final String[] OPS = {"&=", "|=", "^=", "<<=", ">>=", ">>>="};
+
+ private static long apply(String op, long lhs, long rhs) {
+ return switch (op) {
+ case "&=" -> lhs & rhs;
+ case "|=" -> lhs | rhs;
+ case "^=" -> lhs ^ rhs;
+ case "<<=" -> lhs << rhs;
+ case ">>=" -> lhs >> rhs;
+ case ">>>=" -> lhs >>> rhs;
+ default -> throw new AssertionError(op);
+ };
+ }
+
+ @Test public void testLocals() {
+ for (String op : OPS)
+ for (long arg : new long[]{-8, 0, 5, 64}) {
+ long expected = apply(op, arg, 3);
+ check("int x=arg; int r=(x " + op + " 3); return x*10+r;",
+ arg, expected*11);
+ }
+ }
+
+ @Test public void testRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int x=arg; int count=0; int r=(x " + op +
+ " (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11);
+ }
+ }
+
+ @Test public void testNarrowingAndChaining() {
+ check("u8 x=255; x <<= 1; return x;", 0, 254);
+ check("u8 x=255; x ^= 256; return x;", 0, 255);
+ check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312);
+ check("int x=-8; x >>= 65; return x;", 0, -4);
+ check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3);
+ }
+
+ @Test public void testExistingOperators() {
+ check("int x=8; x+=2; x-=1; x*=3; x/=9; return x;", 0, 3);
+ check("int x=arg; int y=x++; int z=x--; return x*100+y*10+z;", 5, 556);
+ check("return (arg << 2) + (arg >> 1) + (arg >>> 1);", 8, 40);
+ check("return arg <= 8;", 8, 1);
+ check("return arg >= 8;", 8, 1);
+ }
+
+ @Test public void testRejectedAssignments() {
+ for (String op : OPS) {
+ rejected("val x=3; x " + op + " 1; return x;");
+ rejected("int x=3; x " + op + " 1.5; return x;");
+ }
+ }
+
+ private static void rejected(String src) {
+ try {
+ check(src, 0, 0);
+ fail("Accepted invalid assignment: " + src);
+ } catch (RuntimeException e) {
+ assertTrue("Must report a language error: " + e, e instanceof Parser.ParseException);
+ assertNotNull(e.getMessage());
+ assertFalse("Must report a language error: " + e, e.getMessage().contains("Not yet implemented"));
+ }
+ }
+
+ @Test public void testFields() {
+ for (String op : OPS) {
+ long expected = apply(op, -8, 3);
+ check("struct S { int x; }; S !s=new S; s.x=arg; int r=(s.x " +
+ op + " 3); return s.x*10+r;", -8, expected*11);
+ }
+ }
+
+ @Test public void testArrayAddressAndRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int[] !a=new int[2]; a[0]=arg; int i=0; int count=0; " +
+ "int r=(a[i++] " + op + " (count=count+1)); " +
+ "return i*1000+count*100+a[0]*10+r;", 5, 1100+expected*11);
+ }
+ }
+
+ private static void check(String src, long arg, long expected) {
+ var code = new CodeGen(src).parse().opto().typeCheck();
+ assertEquals(src, Long.toString(expected), Eval2.eval(code, arg));
+ }
+}
diff --git a/chapter22/src/main/java/com/seaofnodes/simple/Parser.java b/chapter22/src/main/java/com/seaofnodes/simple/Parser.java
index 23e9ddee..c26b9064 100644
--- a/chapter22/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/chapter22/src/main/java/com/seaofnodes/simple/Parser.java
@@ -1172,11 +1172,17 @@ public class Parser {
// flt + flt ==>> use float op
Node op = switch(ch) {
case 1, (char)-1,
- '+' -> new AddNode(lhs,rhs);
- case '-' -> new SubNode(lhs,rhs);
- case '*' -> new MulNode(lhs,rhs);
- case '/' -> new DivNode(lhs,rhs);
- default -> throw Utils.TODO();
+ '+' -> new AddNode(lhs,rhs);
+ case '-' -> new SubNode(lhs,rhs);
+ case '*' -> new MulNode(lhs,rhs);
+ case '/' -> new DivNode(lhs,rhs);
+ case '&' -> new AndNode(loc(),lhs,rhs);
+ case '|' -> new OrNode(loc(),lhs,rhs);
+ case '^' -> new XorNode(loc(),lhs,rhs);
+ case Lexer.SHL -> new ShlNode(loc(),lhs,rhs);
+ case Lexer.SAR -> new SarNode(loc(),lhs,rhs);
+ case Lexer.SHR -> new ShrNode(loc(),lhs,rhs);
+ default -> throw Utils.TODO();
};
// Convert to float ops, or narrow int types; error if not declared type.
// Also, if postfix LHS is still keep()
@@ -1546,16 +1552,44 @@ public class Parser {
private String parseString() {
if( !peek('"') ) return null;
_lexer.inc();
- int start = pos();
- while( !_lexer.isEOF() && _lexer.nextChar()!= '"' ) ;
- if( _lexer.isEOF() )
- throw error("Unclosed string");
- return new String(_lexer._input,start,pos()-start-1);
+ StringBuilder sb = new StringBuilder();
+ while( !_lexer.isEOF() ) {
+ char c = _lexer.nextChar();
+ if( c=='"' ) return sb.toString();
+ if( c!='\\' ) {
+ sb.append(c);
+ continue;
+ }
+ if( _lexer.isEOF() )
+ throw error("Unclosed string");
+ sb.append(escapedChar("string"));
+ }
+ throw error("Unclosed string");
+ }
+
+ private char escapedChar(String kind) {
+ char esc = _lexer.nextChar();
+ return switch( esc ) {
+ case '0' -> '\u0000';
+ case 'n' -> '\n';
+ case 't' -> '\t';
+ case 'r' -> '\r';
+ case '\\' -> '\\';
+ case '"' -> '"';
+ case '\'' -> '\'';
+ default -> throw error("Unknown "+kind+" escape \\\\"+esc);
+ };
}
// Already parsed "'"
private Node parseChar() {
- return require(con(TypeInteger.constant(_lexer.nextChar())),"'");
+ if( _lexer.isEOF() ) throw error("Unclosed character");
+ char c = _lexer.nextChar();
+ if( c=='\\' ) {
+ if( _lexer.isEOF() ) throw error("Unclosed character");
+ c = escapedChar("character");
+ }
+ return require(con(TypeInteger.constant(c)),"'");
}
//////////////////////////////////
@@ -1834,8 +1868,14 @@ public class Parser {
// Next oper= character, or 0.
// As a convenience, mark "++" as a char 1 and "--" as char -1 (65535)
+ // Distinct tags for the multi-character compound operators.
+ static final char SHL = 2, SAR = 3, SHR = 4;
+
public char matchOperAssign() {
skipWhiteSpace();
+ if( match("<<=" ) ) return SHL;
+ if( match(">>>=") ) return SHR;
+ if( match(">>=" ) ) return SAR;
if( _position+2 >= _input.length ) return 0;
char ch0 = (char)_input[_position];
if( "+-/*&|^".indexOf(ch0) == -1 ) return 0;
diff --git a/chapter22/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/chapter22/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
new file mode 100644
index 00000000..0d67d3c5
--- /dev/null
+++ b/chapter22/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class CompoundAssignmentTest {
+ private static final String[] OPS = {"&=", "|=", "^=", "<<=", ">>=", ">>>="};
+
+ private static long apply(String op, long lhs, long rhs) {
+ return switch (op) {
+ case "&=" -> lhs & rhs;
+ case "|=" -> lhs | rhs;
+ case "^=" -> lhs ^ rhs;
+ case "<<=" -> lhs << rhs;
+ case ">>=" -> lhs >> rhs;
+ case ">>>=" -> lhs >>> rhs;
+ default -> throw new AssertionError(op);
+ };
+ }
+
+ @Test public void testLocals() {
+ for (String op : OPS)
+ for (long arg : new long[]{-8, 0, 5, 64}) {
+ long expected = apply(op, arg, 3);
+ check("int x=arg; int r=(x " + op + " 3); return x*10+r;",
+ arg, expected*11);
+ }
+ }
+
+ @Test public void testRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int x=arg; int count=0; int r=(x " + op +
+ " (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11);
+ }
+ }
+
+ @Test public void testNarrowingAndChaining() {
+ check("u8 x=255; x <<= 1; return x;", 0, 254);
+ check("u8 x=255; x ^= 256; return x;", 0, 255);
+ check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312);
+ check("int x=-8; x >>= 65; return x;", 0, -4);
+ check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3);
+ }
+
+ @Test public void testExistingOperators() {
+ check("int x=8; x+=2; x-=1; x*=3; x/=9; return x;", 0, 3);
+ check("int x=arg; int y=x++; int z=x--; return x*100+y*10+z;", 5, 556);
+ check("return (arg << 2) + (arg >> 1) + (arg >>> 1);", 8, 40);
+ check("return arg <= 8;", 8, 1);
+ check("return arg >= 8;", 8, 1);
+ }
+
+ @Test public void testRejectedAssignments() {
+ for (String op : OPS) {
+ rejected("val x=3; x " + op + " 1; return x;");
+ rejected("int x=3; x " + op + " 1.5; return x;");
+ }
+ }
+
+ private static void rejected(String src) {
+ try {
+ check(src, 0, 0);
+ fail("Accepted invalid assignment: " + src);
+ } catch (RuntimeException e) {
+ assertTrue("Must report a language error: " + e, e instanceof Parser.ParseException);
+ assertNotNull(e.getMessage());
+ assertFalse("Must report a language error: " + e, e.getMessage().contains("Not yet implemented"));
+ }
+ }
+
+ @Test public void testFields() {
+ for (String op : OPS) {
+ long expected = apply(op, -8, 3);
+ check("struct S { int x; }; S !s=new S; s.x=arg; int r=(s.x " +
+ op + " 3); return s.x*10+r;", -8, expected*11);
+ }
+ }
+
+ @Test public void testArrayAddressAndRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int[] !a=new int[2]; a[0]=arg; int i=0; int count=0; " +
+ "int r=(a[i++] " + op + " (count=count+1)); " +
+ "return i*1000+count*100+a[0]*10+r;", 5, 1100+expected*11);
+ }
+ }
+
+ private static void check(String src, long arg, long expected) {
+ var code = new CodeGen(src).parse().opto().typeCheck();
+ assertEquals(src, Long.toString(expected), Eval2.eval(code, arg));
+ }
+}
diff --git a/chapter22/src/test/java/com/seaofnodes/simple/LiteralTest.java b/chapter22/src/test/java/com/seaofnodes/simple/LiteralTest.java
new file mode 100644
index 00000000..2375f213
--- /dev/null
+++ b/chapter22/src/test/java/com/seaofnodes/simple/LiteralTest.java
@@ -0,0 +1,65 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeConAryB;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeMemPtr;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class LiteralTest {
+ private static final String[] ESCAPES = {"\\0", "\\n", "\\t", "\\r", "\\\\", "\\\"", "\\'"};
+ private static final int[] VALUES = {0, 10, 9, 13, 92, 34, 39};
+
+ @Test public void testStringEscapes() {
+ for (int i = 0; i < ESCAPES.length; i++)
+ assertString(ESCAPES[i], new byte[]{(byte)VALUES[i]});
+ assertString("a\\\"b\\\\c\\0d", new byte[]{97, 34, 98, 92, 99, 0, 100});
+ assertString("", new byte[]{});
+ assertString("plain", new byte[]{112, 108, 97, 105, 110});
+ }
+
+ private static void assertString(String text, byte[] expected) {
+ var code = new CodeGen("return \"" + text + "\";").parse().opto().typeCheck();
+ TypeMemPtr ptr = (TypeMemPtr)code.expr()._type;
+ TypeConAryB bytes = (TypeConAryB)ptr._obj._con;
+ assertArrayEquals(text, expected, bytes._ary);
+ }
+
+ @Test public void testCharacterEscapes() {
+ for (int i = 0; i < ESCAPES.length; i++)
+ assertCharacter(ESCAPES[i], VALUES[i]);
+ assertCharacter("A", 65);
+ }
+
+ private static void assertCharacter(String text, int expected) {
+ var code = new CodeGen("return '" + text + "';").parse().opto().typeCheck();
+ assertEquals(text, expected, ((TypeInteger)code.expr()._type).value());
+ }
+
+ @Test public void testMalformedStrings() {
+ assertBadLiteral("return \"\\q\";", "Unknown string escape");
+ assertBadLiteral("return \"abc", "Unclosed string");
+ assertBadLiteral("return \"abc\\", "Unclosed string");
+ // The quote closes the string at EOF; the missing token is the semicolon.
+ assertBadLiteral("return \"plain\"", ";");
+ }
+
+ @Test public void testMalformedCharacters() {
+ assertBadLiteral("return '\\q';", "Unknown character escape");
+ assertBadLiteral("return '", "Unclosed character");
+ assertBadLiteral("return '\\", "Unclosed character");
+ assertBadLiteral("return '';", "Syntax error");
+ assertBadLiteral("return 'ab';", "Syntax error");
+ assertBadLiteral("return 'a", "Syntax error");
+ }
+
+ private static void assertBadLiteral(String src, String message) {
+ try {
+ new CodeGen(src).parse();
+ fail("Accepted malformed literal: " + src);
+ } catch (Parser.ParseException e) {
+ assertTrue(e.getMessage(), e.getMessage().contains(message));
+ }
+ }
+}
diff --git a/chapter23/src/main/java/com/seaofnodes/simple/Parser.java b/chapter23/src/main/java/com/seaofnodes/simple/Parser.java
index 76898bea..beea1dcf 100644
--- a/chapter23/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/chapter23/src/main/java/com/seaofnodes/simple/Parser.java
@@ -1253,6 +1253,8 @@ public class Parser {
if( ch==0 ) // Normal primary, check for postfix updates
return parsePostfix(rvalue);
// Assign-update direct into Scope
+ if( var._final )
+ throw error("Cannot reassign final '"+var._name+"'");
Node op = opAssign(ch,rvalue, var.type() );
_scope.update(var,op);
return postfix(ch) ? rvalue.unkeep() : op;
@@ -1303,11 +1305,17 @@ public class Parser {
// flt + flt ==>> use float op
Node op = switch(ch) {
case 1, (char)-1,
- '+' -> new AddNode(lhs,rhs);
- case '-' -> new SubNode(lhs,rhs);
- case '*' -> new MulNode(lhs,rhs);
- case '/' -> new DivNode(lhs,rhs);
- default -> throw Utils.TODO();
+ '+' -> new AddNode(lhs,rhs);
+ case '-' -> new SubNode(lhs,rhs);
+ case '*' -> new MulNode(lhs,rhs);
+ case '/' -> new DivNode(lhs,rhs);
+ case '&' -> new AndNode(loc(),lhs,rhs);
+ case '|' -> new OrNode(loc(),lhs,rhs);
+ case '^' -> new XorNode(loc(),lhs,rhs);
+ case Lexer.SHL -> new ShlNode(loc(),lhs,rhs);
+ case Lexer.SAR -> new SarNode(loc(),lhs,rhs);
+ case Lexer.SHR -> new ShrNode(loc(),lhs,rhs);
+ default -> throw Utils.TODO();
};
// Convert to float ops, or narrow int types; error if not declared type.
// Also, if postfix LHS is still keep()
@@ -1713,16 +1721,44 @@ public class Parser {
private String parseString() {
if( !peek('"') ) return null;
_lexer.inc();
- int start = pos();
- while( !_lexer.isEOF() && _lexer.nextChar()!= '"' ) ;
- if( _lexer.isEOF() )
- throw error("Unclosed string");
- return new String(_lexer._input,start,pos()-start-1);
+ StringBuilder sb = new StringBuilder();
+ while( !_lexer.isEOF() ) {
+ char c = _lexer.nextChar();
+ if( c=='"' ) return sb.toString();
+ if( c!='\\' ) {
+ sb.append(c);
+ continue;
+ }
+ if( _lexer.isEOF() )
+ throw error("Unclosed string");
+ sb.append(escapedChar("string"));
+ }
+ throw error("Unclosed string");
+ }
+
+ private char escapedChar(String kind) {
+ char esc = _lexer.nextChar();
+ return switch( esc ) {
+ case '0' -> '\u0000';
+ case 'n' -> '\n';
+ case 't' -> '\t';
+ case 'r' -> '\r';
+ case '\\' -> '\\';
+ case '"' -> '"';
+ case '\'' -> '\'';
+ default -> throw error("Unknown "+kind+" escape \\\\"+esc);
+ };
}
// Already parsed "'"
private Node parseChar() {
- return require(con(TypeInteger.constant(_lexer.nextChar())),"'");
+ if( _lexer.isEOF() ) throw error("Unclosed character");
+ char c = _lexer.nextChar();
+ if( c=='\\' ) {
+ if( _lexer.isEOF() ) throw error("Unclosed character");
+ c = escapedChar("character");
+ }
+ return require(con(TypeInteger.constant(c)),"'");
}
//////////////////////////////////
@@ -2012,8 +2048,14 @@ public class Parser {
// Next oper= character, or 0.
// As a convenience, mark "++" as a char 1 and "--" as char -1 (65535)
+ // Distinct tags for the multi-character compound operators.
+ static final char SHL = 2, SAR = 3, SHR = 4;
+
public char matchOperAssign() {
skipWhiteSpace();
+ if( match("<<=" ) ) return SHL;
+ if( match(">>>=") ) return SHR;
+ if( match(">>=" ) ) return SAR;
if( _position+2 >= _input.length ) return 0;
char ch0 = (char)_input[_position];
if( "+-/*&|^".indexOf(ch0) == -1 ) return 0;
diff --git a/chapter23/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java b/chapter23/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java
index fd2b2635..9e5f0a2d 100644
--- a/chapter23/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java
+++ b/chapter23/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java
@@ -15,7 +15,7 @@ public class TypeConAryB extends TypeConAry<byte[]> {
@Override public int len() { return _ary.length; }
@Override public int log_size() { return 0; }
@Override public String str() { return "[\""+new String(_ary)+"\"]"; }
- @Override boolean eq(Type t) { return Arrays.equals(_ary,((TypeConAryB)t)._ary); }
+ @Override boolean eq(Type t) { return t instanceof TypeConAryB ary && Arrays.equals(_ary,ary._ary); }
@Override int hash() { return Arrays.hashCode(_ary); }
@Override public void write( ByteArrayOutputStream baos ) { baos.write(_ary,0,_ary.length); }
}
diff --git a/chapter23/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/chapter23/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
new file mode 100644
index 00000000..0d67d3c5
--- /dev/null
+++ b/chapter23/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class CompoundAssignmentTest {
+ private static final String[] OPS = {"&=", "|=", "^=", "<<=", ">>=", ">>>="};
+
+ private static long apply(String op, long lhs, long rhs) {
+ return switch (op) {
+ case "&=" -> lhs & rhs;
+ case "|=" -> lhs | rhs;
+ case "^=" -> lhs ^ rhs;
+ case "<<=" -> lhs << rhs;
+ case ">>=" -> lhs >> rhs;
+ case ">>>=" -> lhs >>> rhs;
+ default -> throw new AssertionError(op);
+ };
+ }
+
+ @Test public void testLocals() {
+ for (String op : OPS)
+ for (long arg : new long[]{-8, 0, 5, 64}) {
+ long expected = apply(op, arg, 3);
+ check("int x=arg; int r=(x " + op + " 3); return x*10+r;",
+ arg, expected*11);
+ }
+ }
+
+ @Test public void testRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int x=arg; int count=0; int r=(x " + op +
+ " (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11);
+ }
+ }
+
+ @Test public void testNarrowingAndChaining() {
+ check("u8 x=255; x <<= 1; return x;", 0, 254);
+ check("u8 x=255; x ^= 256; return x;", 0, 255);
+ check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312);
+ check("int x=-8; x >>= 65; return x;", 0, -4);
+ check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3);
+ }
+
+ @Test public void testExistingOperators() {
+ check("int x=8; x+=2; x-=1; x*=3; x/=9; return x;", 0, 3);
+ check("int x=arg; int y=x++; int z=x--; return x*100+y*10+z;", 5, 556);
+ check("return (arg << 2) + (arg >> 1) + (arg >>> 1);", 8, 40);
+ check("return arg <= 8;", 8, 1);
+ check("return arg >= 8;", 8, 1);
+ }
+
+ @Test public void testRejectedAssignments() {
+ for (String op : OPS) {
+ rejected("val x=3; x " + op + " 1; return x;");
+ rejected("int x=3; x " + op + " 1.5; return x;");
+ }
+ }
+
+ private static void rejected(String src) {
+ try {
+ check(src, 0, 0);
+ fail("Accepted invalid assignment: " + src);
+ } catch (RuntimeException e) {
+ assertTrue("Must report a language error: " + e, e instanceof Parser.ParseException);
+ assertNotNull(e.getMessage());
+ assertFalse("Must report a language error: " + e, e.getMessage().contains("Not yet implemented"));
+ }
+ }
+
+ @Test public void testFields() {
+ for (String op : OPS) {
+ long expected = apply(op, -8, 3);
+ check("struct S { int x; }; S !s=new S; s.x=arg; int r=(s.x " +
+ op + " 3); return s.x*10+r;", -8, expected*11);
+ }
+ }
+
+ @Test public void testArrayAddressAndRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int[] !a=new int[2]; a[0]=arg; int i=0; int count=0; " +
+ "int r=(a[i++] " + op + " (count=count+1)); " +
+ "return i*1000+count*100+a[0]*10+r;", 5, 1100+expected*11);
+ }
+ }
+
+ private static void check(String src, long arg, long expected) {
+ var code = new CodeGen(src).parse().opto().typeCheck();
+ assertEquals(src, Long.toString(expected), Eval2.eval(code, arg));
+ }
+}
diff --git a/chapter23/src/test/java/com/seaofnodes/simple/LiteralTest.java b/chapter23/src/test/java/com/seaofnodes/simple/LiteralTest.java
new file mode 100644
index 00000000..09ee30f5
--- /dev/null
+++ b/chapter23/src/test/java/com/seaofnodes/simple/LiteralTest.java
@@ -0,0 +1,65 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeConAryB;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeMemPtr;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class LiteralTest {
+ private static final String[] ESCAPES = {"\\0", "\\n", "\\t", "\\r", "\\\\", "\\\"", "\\'"};
+ private static final int[] VALUES = {0, 10, 9, 13, 92, 34, 39};
+
+ @Test public void testStringEscapes() {
+ for (int i = 0; i < ESCAPES.length; i++)
+ assertString(ESCAPES[i], new byte[]{(byte)VALUES[i]});
+ assertString("a\\\"b\\\\c\\0d", new byte[]{97, 34, 98, 92, 99, 0, 100});
+ assertString("", new byte[]{});
+ assertString("plain", new byte[]{112, 108, 97, 105, 110});
+ }
+
+ private static void assertString(String text, byte[] expected) {
+ var code = new CodeGen("return \"" + text + "\";").parse().opto().typeCheck();
+ TypeMemPtr ptr = (TypeMemPtr)code.expr()._type;
+ TypeConAryB bytes = (TypeConAryB)ptr._obj.field("[]")._t;
+ assertArrayEquals(text, expected, bytes._ary);
+ }
+
+ @Test public void testCharacterEscapes() {
+ for (int i = 0; i < ESCAPES.length; i++)
+ assertCharacter(ESCAPES[i], VALUES[i]);
+ assertCharacter("A", 65);
+ }
+
+ private static void assertCharacter(String text, int expected) {
+ var code = new CodeGen("return '" + text + "';").parse().opto().typeCheck();
+ assertEquals(text, expected, ((TypeInteger)code.expr()._type).value());
+ }
+
+ @Test public void testMalformedStrings() {
+ assertBadLiteral("return \"\\q\";", "Unknown string escape");
+ assertBadLiteral("return \"abc", "Unclosed string");
+ assertBadLiteral("return \"abc\\", "Unclosed string");
+ // The quote closes the string at EOF; the missing token is the semicolon.
+ assertBadLiteral("return \"plain\"", ";");
+ }
+
+ @Test public void testMalformedCharacters() {
+ assertBadLiteral("return '\\q';", "Unknown character escape");
+ assertBadLiteral("return '", "Unclosed character");
+ assertBadLiteral("return '\\", "Unclosed character");
+ assertBadLiteral("return '';", "Syntax error");
+ assertBadLiteral("return 'ab';", "Syntax error");
+ assertBadLiteral("return 'a", "Syntax error");
+ }
+
+ private static void assertBadLiteral(String src, String message) {
+ try {
+ new CodeGen(src).parse();
+ fail("Accepted malformed literal: " + src);
+ } catch (Parser.ParseException e) {
+ assertTrue(e.getMessage(), e.getMessage().contains(message));
+ }
+ }
+}
diff --git a/chapter24/src/main/java/com/seaofnodes/simple/Parser.java b/chapter24/src/main/java/com/seaofnodes/simple/Parser.java
index 42aac299..1932772c 100644
--- a/chapter24/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/chapter24/src/main/java/com/seaofnodes/simple/Parser.java
@@ -1311,6 +1311,8 @@ public class Parser {
if( ch==0 ) // Normal primary, check for postfix updates
return parsePostfix(rvalue);
// Assign-update direct into Scope
+ if( var._final )
+ throw error("Cannot reassign final '"+var._name+"'");
Node op = opAssign(ch,rvalue, var.type() );
_scope.update(var,op);
return postfix(ch) ? rvalue.unkeep() : op;
@@ -1361,11 +1363,17 @@ public class Parser {
// flt + flt ==>> use float op
Node op = switch(ch) {
case 1, (char)-1,
- '+' -> new AddNode(lhs,rhs);
- case '-' -> new SubNode(lhs,rhs);
- case '*' -> new MulNode(lhs,rhs);
- case '/' -> new DivNode(lhs,rhs);
- default -> throw Utils.TODO();
+ '+' -> new AddNode(lhs,rhs);
+ case '-' -> new SubNode(lhs,rhs);
+ case '*' -> new MulNode(lhs,rhs);
+ case '/' -> new DivNode(lhs,rhs);
+ case '&' -> new AndNode(loc(),lhs,rhs);
+ case '|' -> new OrNode(loc(),lhs,rhs);
+ case '^' -> new XorNode(loc(),lhs,rhs);
+ case Lexer.SHL -> new ShlNode(loc(),lhs,rhs);
+ case Lexer.SAR -> new SarNode(loc(),lhs,rhs);
+ case Lexer.SHR -> new ShrNode(loc(),lhs,rhs);
+ default -> throw Utils.TODO();
};
// Convert to float ops, or narrow int types; error if not declared type.
// Also, if postfix LHS is still keep()
@@ -1772,16 +1780,44 @@ public class Parser {
private String parseString() {
if( !peek('"') ) return null;
_lexer.inc();
- int start = pos();
- while( !_lexer.isEOF() && _lexer.nextChar()!= '"' ) ;
- if( _lexer.isEOF() )
- throw error("Unclosed string");
- return new String(_lexer._input,start,pos()-start-1);
+ StringBuilder sb = new StringBuilder();
+ while( !_lexer.isEOF() ) {
+ char c = _lexer.nextChar();
+ if( c=='"' ) return sb.toString();
+ if( c!='\\' ) {
+ sb.append(c);
+ continue;
+ }
+ if( _lexer.isEOF() )
+ throw error("Unclosed string");
+ sb.append(escapedChar("string"));
+ }
+ throw error("Unclosed string");
+ }
+
+ private char escapedChar(String kind) {
+ char esc = _lexer.nextChar();
+ return switch( esc ) {
+ case '0' -> '\u0000';
+ case 'n' -> '\n';
+ case 't' -> '\t';
+ case 'r' -> '\r';
+ case '\\' -> '\\';
+ case '"' -> '"';
+ case '\'' -> '\'';
+ default -> throw error("Unknown "+kind+" escape \\\\"+esc);
+ };
}
// Already parsed "'"
private Node parseChar() {
- return require(con(TypeInteger.constant(_lexer.nextChar())),"'");
+ if( _lexer.isEOF() ) throw error("Unclosed character");
+ char c = _lexer.nextChar();
+ if( c=='\\' ) {
+ if( _lexer.isEOF() ) throw error("Unclosed character");
+ c = escapedChar("character");
+ }
+ return require(con(TypeInteger.constant(c)),"'");
}
//////////////////////////////////
@@ -2071,8 +2107,14 @@ public class Parser {
// Next oper= character, or 0.
// As a convenience, mark "++" as a char 1 and "--" as char -1 (65535)
+ // Distinct tags for the multi-character compound operators.
+ static final char SHL = 2, SAR = 3, SHR = 4;
+
public char matchOperAssign() {
skipWhiteSpace();
+ if( match("<<=" ) ) return SHL;
+ if( match(">>>=") ) return SHR;
+ if( match(">>=" ) ) return SAR;
if( _position+2 >= _input.length ) return 0;
char ch0 = (char)_input[_position];
if( "+-/*&|^".indexOf(ch0) == -1 ) return 0;
diff --git a/chapter24/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/chapter24/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
new file mode 100644
index 00000000..0d67d3c5
--- /dev/null
+++ b/chapter24/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class CompoundAssignmentTest {
+ private static final String[] OPS = {"&=", "|=", "^=", "<<=", ">>=", ">>>="};
+
+ private static long apply(String op, long lhs, long rhs) {
+ return switch (op) {
+ case "&=" -> lhs & rhs;
+ case "|=" -> lhs | rhs;
+ case "^=" -> lhs ^ rhs;
+ case "<<=" -> lhs << rhs;
+ case ">>=" -> lhs >> rhs;
+ case ">>>=" -> lhs >>> rhs;
+ default -> throw new AssertionError(op);
+ };
+ }
+
+ @Test public void testLocals() {
+ for (String op : OPS)
+ for (long arg : new long[]{-8, 0, 5, 64}) {
+ long expected = apply(op, arg, 3);
+ check("int x=arg; int r=(x " + op + " 3); return x*10+r;",
+ arg, expected*11);
+ }
+ }
+
+ @Test public void testRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int x=arg; int count=0; int r=(x " + op +
+ " (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11);
+ }
+ }
+
+ @Test public void testNarrowingAndChaining() {
+ check("u8 x=255; x <<= 1; return x;", 0, 254);
+ check("u8 x=255; x ^= 256; return x;", 0, 255);
+ check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312);
+ check("int x=-8; x >>= 65; return x;", 0, -4);
+ check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3);
+ }
+
+ @Test public void testExistingOperators() {
+ check("int x=8; x+=2; x-=1; x*=3; x/=9; return x;", 0, 3);
+ check("int x=arg; int y=x++; int z=x--; return x*100+y*10+z;", 5, 556);
+ check("return (arg << 2) + (arg >> 1) + (arg >>> 1);", 8, 40);
+ check("return arg <= 8;", 8, 1);
+ check("return arg >= 8;", 8, 1);
+ }
+
+ @Test public void testRejectedAssignments() {
+ for (String op : OPS) {
+ rejected("val x=3; x " + op + " 1; return x;");
+ rejected("int x=3; x " + op + " 1.5; return x;");
+ }
+ }
+
+ private static void rejected(String src) {
+ try {
+ check(src, 0, 0);
+ fail("Accepted invalid assignment: " + src);
+ } catch (RuntimeException e) {
+ assertTrue("Must report a language error: " + e, e instanceof Parser.ParseException);
+ assertNotNull(e.getMessage());
+ assertFalse("Must report a language error: " + e, e.getMessage().contains("Not yet implemented"));
+ }
+ }
+
+ @Test public void testFields() {
+ for (String op : OPS) {
+ long expected = apply(op, -8, 3);
+ check("struct S { int x; }; S !s=new S; s.x=arg; int r=(s.x " +
+ op + " 3); return s.x*10+r;", -8, expected*11);
+ }
+ }
+
+ @Test public void testArrayAddressAndRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int[] !a=new int[2]; a[0]=arg; int i=0; int count=0; " +
+ "int r=(a[i++] " + op + " (count=count+1)); " +
+ "return i*1000+count*100+a[0]*10+r;", 5, 1100+expected*11);
+ }
+ }
+
+ private static void check(String src, long arg, long expected) {
+ var code = new CodeGen(src).parse().opto().typeCheck();
+ assertEquals(src, Long.toString(expected), Eval2.eval(code, arg));
+ }
+}
diff --git a/chapter24/src/test/java/com/seaofnodes/simple/LiteralTest.java b/chapter24/src/test/java/com/seaofnodes/simple/LiteralTest.java
new file mode 100644
index 00000000..09ee30f5
--- /dev/null
+++ b/chapter24/src/test/java/com/seaofnodes/simple/LiteralTest.java
@@ -0,0 +1,65 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.TypeConAryB;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeMemPtr;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class LiteralTest {
+ private static final String[] ESCAPES = {"\\0", "\\n", "\\t", "\\r", "\\\\", "\\\"", "\\'"};
+ private static final int[] VALUES = {0, 10, 9, 13, 92, 34, 39};
+
+ @Test public void testStringEscapes() {
+ for (int i = 0; i < ESCAPES.length; i++)
+ assertString(ESCAPES[i], new byte[]{(byte)VALUES[i]});
+ assertString("a\\\"b\\\\c\\0d", new byte[]{97, 34, 98, 92, 99, 0, 100});
+ assertString("", new byte[]{});
+ assertString("plain", new byte[]{112, 108, 97, 105, 110});
+ }
+
+ private static void assertString(String text, byte[] expected) {
+ var code = new CodeGen("return \"" + text + "\";").parse().opto().typeCheck();
+ TypeMemPtr ptr = (TypeMemPtr)code.expr()._type;
+ TypeConAryB bytes = (TypeConAryB)ptr._obj.field("[]")._t;
+ assertArrayEquals(text, expected, bytes._ary);
+ }
+
+ @Test public void testCharacterEscapes() {
+ for (int i = 0; i < ESCAPES.length; i++)
+ assertCharacter(ESCAPES[i], VALUES[i]);
+ assertCharacter("A", 65);
+ }
+
+ private static void assertCharacter(String text, int expected) {
+ var code = new CodeGen("return '" + text + "';").parse().opto().typeCheck();
+ assertEquals(text, expected, ((TypeInteger)code.expr()._type).value());
+ }
+
+ @Test public void testMalformedStrings() {
+ assertBadLiteral("return \"\\q\";", "Unknown string escape");
+ assertBadLiteral("return \"abc", "Unclosed string");
+ assertBadLiteral("return \"abc\\", "Unclosed string");
+ // The quote closes the string at EOF; the missing token is the semicolon.
+ assertBadLiteral("return \"plain\"", ";");
+ }
+
+ @Test public void testMalformedCharacters() {
+ assertBadLiteral("return '\\q';", "Unknown character escape");
+ assertBadLiteral("return '", "Unclosed character");
+ assertBadLiteral("return '\\", "Unclosed character");
+ assertBadLiteral("return '';", "Syntax error");
+ assertBadLiteral("return 'ab';", "Syntax error");
+ assertBadLiteral("return 'a", "Syntax error");
+ }
+
+ private static void assertBadLiteral(String src, String message) {
+ try {
+ new CodeGen(src).parse();
+ fail("Accepted malformed literal: " + src);
+ } catch (Parser.ParseException e) {
+ assertTrue(e.getMessage(), e.getMessage().contains(message));
+ }
+ }
+}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/Parser.java b/chapter25/src/main/java/com/seaofnodes/simple/Parser.java
index 1dc50c75..1f03eda6 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/Parser.java
@@ -1978,6 +1978,8 @@ public class Parser {
return parsePostfixMethod(rvalue,_scope.in(_scope.update(_scope.var(selfx),null)));
}
// Assign-update direct into Scope
+ if( var._final )
+ throw error("Cannot reassign final '"+var._name+"'");
Node op = opAssign(ch,rvalue, var.type() );
_scope.update(var,op);
return postfix(ch) ? rvalue.unkeep() : op;
@@ -2001,13 +2003,17 @@ public class Parser {
// flt + flt ==>> use float op
Node op = switch(ch) {
case 1, (char)-1,
- '+' -> new AddNode(lhs,rhs);
- case '-' -> new SubNode(lhs,rhs);
- case '*' -> new MulNode(lhs,rhs);
- case '/' -> new DivNode(lhs,rhs);
- case '|' -> new OrNode(null,lhs,rhs);
- case '&' -> new AndNode(null,lhs,rhs);
- default -> throw TODO();
+ '+' -> new AddNode(lhs,rhs);
+ case '-' -> new SubNode(lhs,rhs);
+ case '*' -> new MulNode(lhs,rhs);
+ case '/' -> new DivNode(lhs,rhs);
+ case '|' -> new OrNode(null,lhs,rhs);
+ case '&' -> new AndNode(null,lhs,rhs);
+ case '^' -> new XorNode(loc(),lhs,rhs);
+ case Lexer.SHL -> new ShlNode(loc(),lhs,rhs);
+ case Lexer.SAR -> new SarNode(loc(),lhs,rhs);
+ case Lexer.SHR -> new ShrNode(loc(),lhs,rhs);
+ default -> throw TODO();
};
// Convert to float ops, or narrow int types; error if not declared type.
// Also, if postfix LHS is still keep()
@@ -2824,8 +2830,14 @@ public class Parser {
// Next oper= character, or 0.
// As a convenience, mark "++" as a char 1 and "--" as char -1 (65535)
+ // Distinct tags for the multi-character compound operators.
+ static final char SHL = 2, SAR = 3, SHR = 4;
+
public char matchOperAssign() {
skipWhiteSpace();
+ if( match("<<=" ) ) return SHL;
+ if( match(">>>=") ) return SHR;
+ if( match(">>=" ) ) return SAR;
if( _position+2 >= _input.length ) return 0;
char ch0 = (char)_input[_position];
if( "+-/*&|^".indexOf(ch0) == -1 ) return 0;
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/chapter25/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
new file mode 100644
index 00000000..0d67d3c5
--- /dev/null
+++ b/chapter25/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class CompoundAssignmentTest {
+ private static final String[] OPS = {"&=", "|=", "^=", "<<=", ">>=", ">>>="};
+
+ private static long apply(String op, long lhs, long rhs) {
+ return switch (op) {
+ case "&=" -> lhs & rhs;
+ case "|=" -> lhs | rhs;
+ case "^=" -> lhs ^ rhs;
+ case "<<=" -> lhs << rhs;
+ case ">>=" -> lhs >> rhs;
+ case ">>>=" -> lhs >>> rhs;
+ default -> throw new AssertionError(op);
+ };
+ }
+
+ @Test public void testLocals() {
+ for (String op : OPS)
+ for (long arg : new long[]{-8, 0, 5, 64}) {
+ long expected = apply(op, arg, 3);
+ check("int x=arg; int r=(x " + op + " 3); return x*10+r;",
+ arg, expected*11);
+ }
+ }
+
+ @Test public void testRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int x=arg; int count=0; int r=(x " + op +
+ " (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11);
+ }
+ }
+
+ @Test public void testNarrowingAndChaining() {
+ check("u8 x=255; x <<= 1; return x;", 0, 254);
+ check("u8 x=255; x ^= 256; return x;", 0, 255);
+ check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312);
+ check("int x=-8; x >>= 65; return x;", 0, -4);
+ check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3);
+ }
+
+ @Test public void testExistingOperators() {
+ check("int x=8; x+=2; x-=1; x*=3; x/=9; return x;", 0, 3);
+ check("int x=arg; int y=x++; int z=x--; return x*100+y*10+z;", 5, 556);
+ check("return (arg << 2) + (arg >> 1) + (arg >>> 1);", 8, 40);
+ check("return arg <= 8;", 8, 1);
+ check("return arg >= 8;", 8, 1);
+ }
+
+ @Test public void testRejectedAssignments() {
+ for (String op : OPS) {
+ rejected("val x=3; x " + op + " 1; return x;");
+ rejected("int x=3; x " + op + " 1.5; return x;");
+ }
+ }
+
+ private static void rejected(String src) {
+ try {
+ check(src, 0, 0);
+ fail("Accepted invalid assignment: " + src);
+ } catch (RuntimeException e) {
+ assertTrue("Must report a language error: " + e, e instanceof Parser.ParseException);
+ assertNotNull(e.getMessage());
+ assertFalse("Must report a language error: " + e, e.getMessage().contains("Not yet implemented"));
+ }
+ }
+
+ @Test public void testFields() {
+ for (String op : OPS) {
+ long expected = apply(op, -8, 3);
+ check("struct S { int x; }; S !s=new S; s.x=arg; int r=(s.x " +
+ op + " 3); return s.x*10+r;", -8, expected*11);
+ }
+ }
+
+ @Test public void testArrayAddressAndRhsOnce() {
+ for (String op : OPS) {
+ long expected = apply(op, 5, 1);
+ check("int[] !a=new int[2]; a[0]=arg; int i=0; int count=0; " +
+ "int r=(a[i++] " + op + " (count=count+1)); " +
+ "return i*1000+count*100+a[0]*10+r;", 5, 1100+expected*11);
+ }
+ }
+
+ private static void check(String src, long arg, long expected) {
+ var code = new CodeGen(src).parse().opto().typeCheck();
+ assertEquals(src, Long.toString(expected), Eval2.eval(code, arg));
+ }
+}
diff --git a/docs/chapter-backports.md b/docs/chapter-backports.md
index 6af70cff..190e5351 100644
--- a/docs/chapter-backports.md
+++ b/docs/chapter-backports.md
@@ -19,9 +19,6 @@ the correction into the chapter's representation, not the entire modern file.
| ID | Change | Proposed destination | Scope and acceptance evidence |
|---|---|---|---|
-| B06 | Decode string escapes; reject unknown/truncated escapes | 22 through 24 | Literal scanner only. Check newline, tab, carriage return, backslash, quote, NUL, and malformed input. |
-| B07 | Decode character escapes; reject truncated literals | 22 through 24 | Separate patch sharing B06's helper. Check values and errors; `parseChar` first appears in 22. |
-| B08 | Complete `&=` and `|=` syntax sugar | 17 onward; **audit** availability of bitwise operators | A small language addition, not a correctness-only patch. Move lexer and assignment expansion together. Check locals, fields, arrays, and exactly-once address evaluation. |
| B09 | Make diagnostic function lookup read-only | 18 onward; **audit** first mutating printer lookup | Separate `lookupFun` from cleanup-performing `link`. Verify printing preserves graph/linker state. Do not import FunPtrNode or compilation units for this fix. |
| B10 | Preserve widening state in integer nonzero refinement | **Audit** first chapter with both widening and `nonZero`; no later than 24 | Chapter 25 `TypeInteger.nonZero` preserves `_widen`. Check lattice laws and loop refinement. Exclude TypeScalar, storage-type, and serialization changes. |
| B11 | Diagnose return types using the optimized return expression | 18; 19 already has the correction | `ReturnNode.err()` uses `expr()._type` instead of the parse-time `mt` aggregate. Chapter 18 rejects `struct S { u8 x; }; return new S; return 0;` with a mixed integer/reference error; Chapter 19 accepts it. Also test genuinely reachable incompatible returns. This is a candidate, not yet an applied or isolated-patch-verified fix. |