diff options
| author | Cliff Click <cliffc@acm.org> | 2026-09-17 16:39:32 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-09-17 16:39:32 -0700 |
| commit | 200162345900136663027b6deae21d72cf30818c (patch) | |
| tree | a9b26f38b5f920f61837327ab56f97c5f9a01016 /chapter22/src | |
| parent | 42681f41318796afc58bc14ce04620b7c5c7eb18 (diff) | |
| download | Simple-200162345900136663027b6deae21d72cf30818c.tar.gz Simple-200162345900136663027b6deae21d72cf30818c.zip | |
Backport string escapes and many op-assign
Diffstat (limited to 'chapter22/src')
3 files changed, 210 insertions, 11 deletions
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)); + } + } +} |
