summaryrefslogtreecommitdiff
path: root/chapter23
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-09-17 16:39:32 -0700
committerCliff Click <cliffc@acm.org>2026-09-17 16:39:32 -0700
commit200162345900136663027b6deae21d72cf30818c (patch)
treea9b26f38b5f920f61837327ab56f97c5f9a01016 /chapter23
parent42681f41318796afc58bc14ce04620b7c5c7eb18 (diff)
downloadSimple-200162345900136663027b6deae21d72cf30818c.tar.gz
Simple-200162345900136663027b6deae21d72cf30818c.zip
Backport string escapes and many op-assign
Diffstat (limited to 'chapter23')
-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
4 files changed, 213 insertions, 12 deletions
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));
+ }
+ }
+}