summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-10-03 14:03:48 -0700
committerCliff Click <cliffc@acm.org>2026-10-08 16:52:27 -0700
commit19b9102cf7290de1e70c962e556caccc4e0fd5d9 (patch)
treed906a5080bd7ca74821f878368bbc66612a1a2f7 /src
parent848fcaac757a631d9300cf45d65175b16c2b0ccb (diff)
downloadSimple-19b9102cf7290de1e70c962e556caccc4e0fd5d9.tar.gz
Simple-19b9102cf7290de1e70c962e556caccc4e0fd5d9.zip
Chapter 17b: Syntax Sugar
Co-authored-by: Jun Kawasaki <jun784@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/seaofnodes/simple/Parser.java198
-rw-r--r--src/main/java/com/seaofnodes/simple/node/LoadNode.java1
-rw-r--r--src/main/java/com/seaofnodes/simple/node/MemPhiNode.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/node/StoreNode.java40
-rw-r--r--src/test/java/com/seaofnodes/simple/BrainFuckTest.java72
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter11Test.java5
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter13Test.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter17aTest.java18
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter17bTest.java549
-rw-r--r--src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java77
-rw-r--r--src/test/java/com/seaofnodes/simple/MutabilityTest.java6
11 files changed, 946 insertions, 27 deletions
diff --git a/src/main/java/com/seaofnodes/simple/Parser.java b/src/main/java/com/seaofnodes/simple/Parser.java
index e2df494e..c02f0305 100644
--- a/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/src/main/java/com/seaofnodes/simple/Parser.java
@@ -149,6 +149,8 @@ public class Parser {
put("u16" ,TypeInteger.U16);
put("u32" ,TypeInteger.U32);
put("u8" ,TypeInteger.U8 );
+ put("val" ,Type.TOP); // Marker type, indicates type inference
+ put("var" ,Type.BOTTOM); // Marker type, indicates type inference
}};
}
@@ -214,6 +216,7 @@ public class Parser {
else if (match ("{") ) return require(parseBlock(false),"}");
else if (matchx("if") ) return parseIf();
else if (matchx("while") ) return parseWhile();
+ else if (matchx("for") ) return parseFor();
else if (matchx("break") ) return parseBreak();
else if (matchx("continue")) return parseContinue();
else if (matchx("struct") ) return parseStruct();
@@ -230,9 +233,38 @@ public class Parser {
* </pre>
* @return a {@link Node}, never {@code null}
*/
-
private Node parseWhile() {
require("(");
+ return parseLooping(false);
+ }
+
+
+ /**
+ * Parses a for statement
+ *
+ * <pre>
+ * for( var x=init; test; incr ) body
+ * </pre>
+ * @return a {@link Node}, never {@code null}
+ */
+ private Node parseFor() {
+ // { var x=init,y=init,...;
+ // while( pred ) {
+ // body;
+ // next;
+ // }
+ // }
+ require("(");
+ _scope.push(); // Scope for the index variables
+ if( !match(";") ) // Can be empty init "for(;test;next) body"
+ parseDeclarationStatement(); // Non-empty init
+ Node rez = parseLooping(true);
+ _scope.pop(); // Exit index variable scope
+ return rez;
+ }
+
+ // Shared by `for` and `while`
+ private Node parseLooping( boolean doFor ) {
var savedContinueScope = _continueScope;
var savedBreakScope = _breakScope;
@@ -256,8 +288,8 @@ public class Parser {
_xScopes.push(_scope = _scope.dup(true)); // The true argument triggers creating phis
// Parse predicate
- var pred = parseAsgn();
- require(")");
+ var pred = peek(';') ? con(1) : parseAsgn();
+ require( doFor ? ";" : ")" );
// IfNode takes current control and predicate
Node ifNode = new IfNode(ctrl(), pred.keep()).peephole();
@@ -265,6 +297,16 @@ public class Parser {
Node ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep();
Node ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole();
+ // for( ;;next ) body
+ int nextPos = -1, nextEnd = -1;
+ if( doFor ) {
+ // Skip the next expression and parse it later
+ nextPos = pos();
+ skipAsgn();
+ nextEnd = pos();
+ require(")");
+ }
+
// Clone the body Scope to create the break/exit Scope which accounts for any
// side effects in the predicate. The break/exit Scope will be the final
// scope after the loop, and its control input is the False branch of
@@ -290,6 +332,16 @@ public class Parser {
_scope = _continueScope;
}
+ // Now append the next code onto the body code
+ if( doFor ) {
+ int old = pos(nextPos);
+ if( !peek(')') )
+ parseAsgn();
+ if( pos() != nextEnd )
+ throw errorSyntax( "Unexpected code after expression" );
+ pos(old);
+ }
+
// The true branch loops back, so whatever is current _scope.ctrl gets
// added to head loop as input. endLoop() updates the head scope, and
// goes through all the phis that were created earlier. For each phi,
@@ -345,6 +397,24 @@ public class Parser {
return _breakScope;
}
+ // Look for an unbalanced `)`, skipping balanced
+ private Type skipAsgn() {
+ int paren=0;
+ while( true )
+ // Next X char handles skipping complex comments
+ switch( _lexer.nextXChar() ) {
+ case Character.MAX_VALUE:
+ throw Utils.TODO();
+ case ')':
+ if( --paren<0 )
+ return posT(pos()-1); // Leave the `)` behind
+ break;
+ case '(': paren ++; break;
+ default: break;
+ }
+ }
+
+
/**
* Parses a statement
*
@@ -357,7 +427,11 @@ public class Parser {
// Parse predicate
require("(");
var pred = require(parseAsgn(), ")");
+ return parseTrinary(pred,true,"else");
+ }
+ // Parse a conditional expression, merging results.
+ private Node parseTrinary( Node pred, boolean stmt, String fside ) {
pred.keep();
// IfNode takes current control and predicate
@@ -375,7 +449,7 @@ public class Parser {
// Parse the true side
ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection
_scope.addGuards(ifT,pred,false); // Up-cast predicate
- parseStatement(); // Parse true-side
+ Node lhs = stmt ? parseStatement() : parseAsgn().keep(); // Parse true-side
_scope.removeGuards(ifT);
ScopeNode tScope = _scope;
@@ -386,8 +460,10 @@ public class Parser {
// Up-cast predicate, even if not else clause, because predicate can
// remain true if the true clause exits: `if( !ptr ) return 0; return ptr.fld;`
_scope.addGuards(ifF,pred,true);
- boolean doRHS = matchx("else");
- if( doRHS ) parseStatement();
+ boolean doRHS = match(fside);
+ Node rhs = doRHS
+ ? (stmt ? parseStatement() : parseAsgn())
+ : (stmt ? null : con(lhs._type.makeZero()));
_scope.removeGuards(ifF);
if( doRHS )
fScope = _scope;
@@ -397,12 +473,19 @@ public class Parser {
if( tScope.nIns() != ndefs || fScope.nIns() != ndefs )
throw error("Cannot define a new name on one arm of an if");
+ // Check the trinary widening int/flt
+ if( !stmt ) {
+ rhs = widenInt( rhs, lhs._type ).keep();
+ lhs = widenInt( lhs.unkeep(), rhs._type ).keep();
+ rhs.unkeep();
+ }
+
// Merge results
_scope = tScope;
_xScopes.pop(); // Discard pushed from graph display
RegionNode r = ctrl(tScope.mergeScopes(fScope));
- Node ret = r;
+ Node ret = stmt ? r : peep(new PhiNode("",lhs._type.meet(rhs._type),r,lhs.unkeep(),rhs));
r.peephole();
return ret;
}
@@ -503,13 +586,15 @@ public class Parser {
private boolean bindingFinal(Type t) {
if( match("!") ) return false;
if( match("~") ) return true;
- return t instanceof TypeMemPtr;
+ return t==Type.TOP || t instanceof TypeMemPtr;
}
/** Parse final: [!|~]var['=' asgn]
*/
private Node parseDeclaration(Type t) {
assert t!=null;
+ // Has var/val instead of a user-declared type
+ boolean inferType = t==Type.TOP || t==Type.BOTTOM;
boolean xfinal = bindingFinal(t);
String name = requireId();
@@ -517,8 +602,13 @@ public class Parser {
Node expr;
if( match("=") ) {
expr = parseAsgn();
+ // var/val, then type comes from expression
+ if( inferType )
+ t = expr._type instanceof TypeMemPtr ptr ? ptr.makeFrom(true) : expr._type.glb();
} else {
+ if( inferType && !_scope.inCon() )
+ throw errorSyntax("=expression");
expr = con(xfinal && !(t instanceof TypeMemPtr) ? Type.TOP : t.makeInit());
}
@@ -581,7 +671,7 @@ public class Parser {
// types (which ARE valid here) from local vars in an (optional) forward
// ref type position.
- // t = int|i8|i16|i32|i64|u8|u16|u32|u64|byte|bool | flt|f32|f64 | struct[?]
+ // t = int|i8|i16|i32|i64|u8|u16|u32|u64|byte|bool | flt|f32|f64 | val | var | struct[?]
private Type type() {
int old1 = pos();
boolean writable = match("!");
@@ -661,11 +751,14 @@ public class Parser {
* Parse an expression of the form:
*
* <pre>
- * expr : bitwise
+ * expr : bitwise [? expr [: expr]]
* </pre>
* @return an expression {@link Node}, never {@code null}
*/
- private Node parseExpression() { return parseBitwise(); }
+ private Node parseExpression() {
+ Node expr = parseBitwise();
+ return match("?") ? parseTrinary(expr,false,":") : expr;
+ }
/**
* Parse an bitwise expression
@@ -792,8 +885,26 @@ public class Parser {
* @return a unary expression {@link Node}, never {@code null}
*/
private Node parseUnary() {
- if( match("-") ) return peep(new MinusNode(parseUnary()).widen());
- if( match("!") ) return peep(new NotNode(parseUnary()));
+ // Pre-dec/pre-inc
+ int old = pos();
+ if( match("--") || match("++") ) {
+ int delta = _lexer.peek(-1)=='+' ? 1 : -1; // Pre vs post
+ String name = _lexer.matchId();
+ if( name!=null ) {
+ ScopeNode.Var n = _scope.lookup(name);
+ if( n != null ) {
+ if( n._final )
+ throw error("Cannot reassign final '"+n._name+"'");
+ Node expr = zsMask(peep(new AddNode(_scope.in(n),con(delta))),n.type());
+ _scope.update(n,expr);
+ return expr;
+ }
+ }
+ // Reset, try again
+ pos(old);
+ }
+ if (match("-")) return peep(new MinusNode(parseUnary()).widen());
+ if (match("!")) return peep(new NotNode(parseUnary()));
return parsePostfix(parsePrimary());
}
@@ -817,7 +928,35 @@ public class Parser {
Node rvalue = _scope.in(n);
if( rvalue._type == Type.BOTTOM )
throw error("Cannot read uninitialized field '"+n._name+"'");
- return rvalue;
+ // Check for assign-update, x += e0;
+ char ch = _lexer.matchOperAssign();
+ if( ch==0 ) return rvalue;
+ 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 '&' -> 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;
+ // Parse RHS argument as needed
+ if( pre )
+ { op.keep().setDef(2,parseAsgn()); op.unkeep(); }
+ else rvalue.keep(); // Keep post-value across peeps
+ op = zsMask(peep(op),n.type());
+ _scope.update(n,op);
+ return pre ? op : rvalue.unkeep();
}
ScopeNode.Var requireLookupId(String msg) {
@@ -999,7 +1138,15 @@ public class Parser {
if( base.isAry() ) load.setDef(0,ctrl());
load = peep(load);
- expr.unkill();
+ // ary[idx]++ or ptr.fld++
+ if( matchx("++") || matchx("--") ) {
+ if( f._final && !f._fname.equals("[]") )
+ throw error("Cannot reassign final '"+f._fname+"'");
+ Node inc = peep(new AddNode(load,con( _lexer.peek(-1)=='+' ? 1 : -1)));
+ Node val = zsMask(inc,tf);
+ store(name,f._alias,tf,expr.unkeep(),off,val,false,base.isAry() ? ctrl() : null);
+ // And use the original loaded value as the result
+ } else expr.unkill();
off.unkill();
return parsePostfix(load);
@@ -1280,5 +1427,26 @@ public class Parser {
return new String(_input, start, 1);
}
+ // 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;
+ char ch1 = (char)_input[_position+1];
+ if( ch1 == '=' ) { _position += 2; return ch0; }
+ if( isIdLetter((char)_input[_position+2]) ) return 0;
+ if( ch0 == '+' && ch1 == '+' ) { _position += 2; return (char) 1; }
+ if( ch0 == '-' && ch1 == '-' ) { _position += 2; return (char)-1; }
+ return 0;
+ }
}
}
diff --git a/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/src/main/java/com/seaofnodes/simple/node/LoadNode.java
index 85ad243b..eaab680c 100644
--- a/src/main/java/com/seaofnodes/simple/node/LoadNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/LoadNode.java
@@ -52,6 +52,7 @@ public class LoadNode extends MemOpNode {
@Override
public Type compute() {
+ if( ptr()._type==Type.TOP ) return Type.TOP; // Dead pointer, not a type error.
Type t = MemMergeNode.contents(mem(),_alias,this);
// Update declared forward ref to the actual.
if( _declaredType.isFRef() && t instanceof TypeMemPtr tmp && !tmp.isFRef() )
diff --git a/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java b/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java
index 6aae00f9..693ad944 100644
--- a/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java
@@ -34,6 +34,11 @@ public class MemPhiNode extends PhiNode {
setDef(i,IterPeeps.add(mem.alias(_alias)));
return this;
}
+ if( nIns()==3 && region() instanceof LoopNode loop && !loop.inProgress() &&
+ in(2) instanceof StoreNode st && st.mem()==this ) {
+ Node sink = st.sink(this);
+ if( sink!=null ) return sink;
+ }
return super.idealize();
}
diff --git a/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/src/main/java/com/seaofnodes/simple/node/StoreNode.java
index a5f04dd2..962618ef 100644
--- a/src/main/java/com/seaofnodes/simple/node/StoreNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/StoreNode.java
@@ -101,6 +101,46 @@ public class StoreNode extends MemOpNode {
return null;
}
+ // Phi(mem, Store(phi,ptr,off,val)) -> Store(mem,ptr,off,Phi(load(mem),val)).
+ // With no observers of the backedge store, only the last value matters.
+ // The entry load preserves the field even when the loop takes zero trips.
+ Node sink(MemPhiNode phi) {
+ addDepForwards(phi);
+ if( nOuts()!=1 ) return null;
+ ptr().addDep(phi);
+ // Start with a fixed field of an allocation dominating the loop.
+ // In particular, do not sink stores through a loop-varying address.
+ if( !(ptr() instanceof ProjNode p) || !(p.in(0) instanceof NewNode obj) ||
+ !(ptr()._type instanceof TypeMemPtr tmp) || tmp._obj.isAry() ||
+ !(off() instanceof ConstantNode) || err()!=null ) return null;
+ LoopNode loop = (LoopNode)phi.region();
+ // Bulk splitting identifies precise slices by Region/alias. Finish it
+ // before replacing this memory point with a Store.
+ for( Node use : loop._outputs )
+ if( use instanceof BulkMemPhiNode ) {
+ use.addDepForwards(phi);
+ return null;
+ }
+ obj.addDep(phi);
+ CFGNode ctrl = loop.entry();
+ while( ctrl!=null && ctrl!=obj.cfg0() ) {
+ ctrl.addDep(phi);
+ ctrl=ctrl.idom(phi);
+ }
+ if( ctrl==null ) return null;
+
+ Node mem = phi.in(1);
+ Node init = new LoadNode(_name,_alias,_declaredType,mem,ptr(),off()).peephole();
+ Node value = new PhiNode(_name,_declaredType,loop,init,val()).peephole();
+ Node sink = new StoreNode(_name,_alias,_declaredType,mem,ptr(),off(),value,_init);
+ // Break the memory cycle, killing the old store. Keep the Phi alive
+ // even if that store was its last use, until our caller replaces it.
+ phi.keep();
+ phi.setDef(2,mem);
+ phi.unkeep();
+ return sink;
+ }
+
// Check that "mem" has no uses except "this"
private boolean checkOnlyUse(Node mem) {
if( mem.nOuts()==1 ) return true;
diff --git a/src/test/java/com/seaofnodes/simple/BrainFuckTest.java b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
new file mode 100644
index 00000000..154b74a2
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
@@ -0,0 +1,72 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.evaluator.Evaluator;
+import com.seaofnodes.simple.node.StopNode;
+import org.junit.Test;
+import org.junit.Ignore;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class BrainFuckTest {
+
+ @Test
+ public void testBrainfuck() {
+ var program = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.".getBytes(StandardCharsets.UTF_8);
+ var encoded = new StringBuilder("u8[] !program = new u8[").append(program.length).append("];");
+ for (int i = 0; i < program.length; i++) {
+ int value = program[i] & 0xFF;
+ encoded.append("program[").append(i).append("] = ").append(value).append(";");
+ }
+
+ Parser parser = new Parser(
+ encoded + """
+var d = 0;
+u8[] !output = new u8[0];
+u8[] !data = new u8[100];
+
+for( int pc = 0; pc < program#; pc++ ) {
+ var command = program[pc];
+ if (command == 62) {
+ d++;
+ } else if (command == 60) {
+ d--;
+ } else if (command == 43) {
+ data[d]++;
+ } else if (command == 45) {
+ data[d]--;
+ } else if (command == 46) {
+ // Output a byte; increase the output array size
+ var old = output;
+ output = new u8[output# + 1];
+ for( var i = 0; i < old#; i++ )
+ output[i] = old[i];
+ output[old#] = data[d]; // Add the extra byte on the end
+ } else if (command == 44) {
+ data[d] = 42;
+ } else if (command == 91) {
+ if (data[d] == 0) {
+ for( var d = 1; d > 0; ) {
+ command = program[++pc];
+ if (command == 91) d++;
+ if (command == 93) d--;
+ }
+ }
+ } else if (command == 93) {
+ if (data[d]) {
+ for( var d = 1; d > 0; ) {
+ command = program[--pc];
+ if (command == 93) d++;
+ if (command == 91) d--;
+ }
+ }
+ }
+}
+return output;
+ """);
+ StopNode stop = parser.parse().iterate();
+ assertEquals("Hello World!\n", Evaluator.evaluate(stop, 0, 10000).toString());
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter11Test.java b/src/test/java/com/seaofnodes/simple/Chapter11Test.java
index 60ff05d8..f0caed02 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter11Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter11Test.java
@@ -34,7 +34,8 @@ public class Chapter11Test {
""").parse().iterate();
int[] phis = {0};
stop.walk(n -> {
- if (n instanceof MemPhiNode) phis[0]++;
+ // Store sinking can replace field memory Phis with value Phis.
+ if (n instanceof PhiNode) phis[0]++;
if (n instanceof MemMergeNode m && m.in(1) instanceof BulkMemPhiNode b)
for(int a=b._aliases.nextSetBit(0); a>=0; a=b._aliases.nextSetBit(a+1))
assertTrue(a<m.nIns() && m.in(a)!=null);
@@ -42,7 +43,7 @@ public class Chapter11Test {
!(n instanceof MemPhiNode) && !(n instanceof BulkMemPhiNode));
return null;
});
- assertTrue(phis[0]>=3);
+ assertTrue(phis[0]>=4); // Three fields and the loop counter.
assertArrayEquals(new Object[]{1L,2L,3L},((Evaluator.Obj)Evaluator.evaluate(stop,4)).fields());
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter13Test.java b/src/test/java/com/seaofnodes/simple/Chapter13Test.java
index 695777e2..8f64701f 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter13Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter13Test.java
@@ -293,7 +293,7 @@ else {
}
""");
StopNode stop = parser.parse().iterate();
- assertEquals("Stop[ return 9; return 0; return 0; ]", stop.toString());
+ assertEquals("Stop[ return 9; return 0; return 0; return 0; return 0; ]", stop.toString());
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter17aTest.java b/src/test/java/com/seaofnodes/simple/Chapter17aTest.java
index 7d61a913..423eade0 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter17aTest.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter17aTest.java
@@ -51,7 +51,7 @@ return p;
Parser parser = new Parser("""
struct Point { int x=3, y=4; };
Point p = new Point { x=5; y=6; };
-p.x=p.x+1;
+p.x++;
return p;
""");
try { parser.parse().iterate(); fail(); }
@@ -63,7 +63,7 @@ return p;
Parser parser = new Parser("""
struct Point { int x=3, y=4; };
Point p = new Point;
-p.x=p.x+1;
+p.x++;
return p;
""");
try { parser.parse().iterate(); fail(); }
@@ -72,8 +72,8 @@ return p;
@Test
public void testStructFinal3() {
- for( String fields : new String[] { "int ~x; int ~y;", "int[] !x;", "int[] !x;" } ) {
- String src = "struct Point { "+fields+" }; Point p = new Point; p.x=p.x+1; return p;";
+ for( String fields : new String[] { "var x; var y;", "int[] !x;", "int[] !x;" } ) {
+ String src = "struct Point { "+fields+" }; Point p = new Point; p.x++; return p;";
try { new Parser(src).parse().iterate(); fail(fields); }
catch( Exception e ) { assertEquals("'Point' is not fully initialized, field 'x' needs to be set in a constructor",e.getMessage()); }
}
@@ -82,21 +82,21 @@ return p;
@Test
public void testStructFinal4() {
Parser parser = new Parser("""
-struct Point { int ~x=3; int ~y=4; };
+struct Point { val x=3; val y=4; };
Point p = new Point;
-p.x=p.x+1;
+p.x++;
return p;
""");
try { parser.parse().iterate(); fail(); }
- catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); }
+ catch( Exception e ) { assertEquals("Cannot reassign final 'x'",e.getMessage()); }
}
@Test
public void testStructFinal5() {
Parser parser = new Parser("""
-struct Point { int x=3; int y=4; };
+struct Point { var x=3; var y=4; };
!Point !p = new Point;
-p.x=p.x+1;
+p.x++;
return p;
""");
StopNode stop = parser.parse().iterate();
diff --git a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
new file mode 100644
index 00000000..64b606d3
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
@@ -0,0 +1,549 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.evaluator.Evaluator;
+import com.seaofnodes.simple.node.StopNode;
+import com.seaofnodes.simple.node.StoreNode;
+import org.junit.Test;
+import org.junit.Ignore;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class Chapter17bTest {
+
+ @Test public void testSinkLoopStores() {
+ StopNode stop = new Parser("""
+ struct Totals { int sum; int visits; };
+ val t = new Totals { sum=7; visits=2; };
+ val data = new int[4];
+ for( int i=0; i<data#; i++ ) data[i]=arg+i;
+ for( int i=0; i<(arg<data# ? arg : data#); i++ ) {
+ t.sum = t.sum + data[i];
+ t.visits++;
+ }
+ return t;
+ """).parse().iterate();
+ stop.typeCheck();
+ int[] stores = {0};
+ stop.walk(n -> {
+ if( n instanceof StoreNode st && !st._name.equals("[]") ) {
+ assertEquals(stop.in(0).in(0),st.in(0)); // Both stores at the exit.
+ stores[0]++;
+ }
+ return null;
+ });
+ assertEquals(2,stores[0]);
+ for( int arg : new int[]{-1,0,1,3,4,5} ) {
+ long n = Math.max(0,Math.min(arg,4));
+ var obj = (Evaluator.Obj)Evaluator.evaluate(stop,arg);
+ org.junit.Assert.assertArrayEquals(new Object[]{7+n*arg+n*(n-1)/2,2+n},obj.fields());
+ }
+ }
+
+ @Test public void testKeepObservedLoopStore() {
+ StopNode stop = new Parser("""
+ struct S { int x; };
+ val a = new S;
+ val b = new S;
+ val p = arg ? a : b;
+ int sum=0;
+ for( int i=0; i<4; i++ ) {
+ a.x=i+1;
+ sum+=p.x;
+ }
+ return sum;
+ """).parse().iterate();
+ stop.typeCheck();
+ assertEquals(0L,Evaluator.evaluate(stop,0));
+ assertEquals(10L,Evaluator.evaluate(stop,1));
+ }
+
+ @Ignore
+ @Test
+ public void testJig() {
+ Parser parser = new Parser("return 1;");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 1;", stop.toString());
+ assertEquals(1L, Evaluator.evaluate(stop, 0));
+ }
+
+
+ // ---------------------------------------------------------------
+ @Test
+ public void testInc0() {
+ Parser parser = new Parser("""
+return arg++;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return arg;", stop.toString());
+ assertEquals(0L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test
+ public void testInc1() {
+ Parser parser = new Parser("""
+return arg+++arg++;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return ((arg*2)+1);", stop.toString());
+ assertEquals(1L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test
+ public void testInc2() {
+ Parser parser = new Parser("""
+// -(arg--)-(arg--)
+return -arg---arg--;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return (-((arg*2)+-1));", stop.toString());
+ assertEquals(1L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test
+ public void testInc3() {
+ Parser parser = new Parser("""
+int[] !xs = new int[arg];
+xs[0]++;
+xs[1]++;
+return xs[0];
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 1;", stop.toString());
+ assertEquals(1L, Evaluator.evaluate(stop, 2));
+ }
+
+ @Test
+ public void testInc4() {
+ Parser parser = new Parser("""
+u8[] !xs = new u8[1];
+xs[0]--;
+return xs[0];
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 255;", stop.toString());
+ assertEquals(255L, Evaluator.evaluate(stop, 2));
+ }
+
+ @Test
+ public void testInc5() {
+ Parser parser = new Parser("""
+struct S { u16 x; };
+!S !s = new S;
+s.x--;
+return s.x;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 65535;", stop.toString());
+ assertEquals(65535L, Evaluator.evaluate(stop, 2));
+ }
+
+ @Test
+ public void testInc6() {
+ Parser parser = new Parser("return --arg;");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return (arg+-1);", stop.toString());
+ assertEquals(-1L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test
+ public void testInc7() {
+ Parser parser = new Parser("u8 x=0; return --x;");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 255;", stop.toString());
+ assertEquals(255L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test
+ public void testInc8() {
+ Parser parser = new Parser("int x; x+=2; return x+=3;");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 5;", stop.toString());
+ assertEquals(5L, Evaluator.evaluate(stop, 0));
+ }
+
+ // ---------------------------------------------------------------
+ @Test public void testVar0() {
+ Parser parser = new Parser("var d; return d;");
+ try { parser.parse().iterate(); fail(); }
+ catch( Exception e ) { assertEquals("Syntax error, expected =expression: ;",e.getMessage()); }
+ }
+
+ @Test public void testVar1() {
+ Parser parser = new Parser("val d; return d;");
+ try { parser.parse().iterate(); fail(); }
+ catch( Exception e ) { assertEquals("Syntax error, expected =expression: ;",e.getMessage()); }
+ }
+
+ @Test public void testVar2() {
+ Parser parser = new Parser("int x; x=3; x++; return x; // Ok, no initializer so x is mutable ");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 4;", stop.toString());
+ assertEquals(4L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test public void testVar3() {
+ Parser parser = new Parser("int x=3; x++; return x; // Ok, primitive so x is mutable despite initializer ");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 4;", stop.toString());
+ assertEquals(4L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test public void testVar4() {
+ Parser parser = new Parser("struct S{int x;}; !S !s; s=new S; s.x++; return s.x; // Ok, no initializer so x is mutable ");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 1;", stop.toString());
+ assertEquals(1L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test public void testVar5() {
+ Parser parser = new Parser("struct S{int x;}; !S !s; s=new S{x=3;}; s.x++; return s.x; // Ok, no initializer so x is mutable ");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 4;", stop.toString());
+ assertEquals(4L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test public void testVar6() {
+ Parser parser = new Parser("struct S{int x;}; S s=new S; s.x++; return s.x; // Error initializer so x is immutable ");
+ try { parser.parse().iterate(); fail(); }
+ catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); }
+ }
+
+ @Test public void testVar7() {
+ Parser parser = new Parser("struct S{int x;}; S s=new S{x=3;}; s.x++; return s.x; // Error initializer so x is immutable ");
+ try { parser.parse().iterate(); fail(); }
+ catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); }
+ }
+
+ @Test public void testVar8() {
+ Parser parser = new Parser("struct S{int x;}; !S !s=new S; s.x++; return s.x; // Ok, has '!' so s.x is mutable ");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 1;", stop.toString());
+ assertEquals(1L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test public void testVar9() {
+ Parser parser = new Parser("struct S{int x;}; var s=new S; s.x++; return s.x; // Ok, has var so s.x is mutable ");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 1;", stop.toString());
+ assertEquals(1L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test public void testVar10() {
+ Parser parser = new Parser("struct S{int x;}; ~S s=new S; s.x++; return s.x; // Error, has val so x is immutable ");
+ try { parser.parse().iterate(); fail(); }
+ catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); }
+ }
+
+ @Test public void testVar11() {
+ Parser parser = new Parser("""
+struct Bar { int x; };
+!Bar !bar = new Bar;
+bar.x = 3; // Ok, bar is mutable
+
+struct Foo { !Bar? !bar; int y; };
+!Foo !foo = new Foo { bar = bar; };
+foo.bar = bar; // Ok foo is mutable
+foo.bar.x++; // Ok foo and foo.bar and foo.bar.x are all mutable
+
+~Foo xfoo = foo; // Throw away mutability
+xfoo.bar.x++; // Error, cannot mutate through xfoo
+ """);
+ try { parser.parse().iterate(); fail(); }
+ catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); }
+ }
+
+ @Test public void testVar12() {
+ Parser parser = new Parser("""
+struct Bar { int x; };
+!Bar !bar = new Bar;
+bar.x = 3; // Ok, bar is mutable
+
+struct Foo { !Bar? !bar; int y; };
+!Foo !foo = new Foo;
+foo.bar = bar; // Ok bar is mutable
+foo.bar.x++; // Ok foo and foo.bar and foo.bar.x are all mutable
+
+~Foo xfoo = foo; // Throw away mutability
+int x4 = xfoo.bar.x; // Ok to read through xfoo, gets 4
+foo.bar.x++; // Bumps to 5
+int x5 = xfoo.bar.x; // Ok to read through xfoo, gets 5
+return x4*10+x5;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 45;", stop.toString());
+ assertEquals(45L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test
+ public void testVar13() {
+ Parser parser = new Parser("""
+int i,i++;
+""");
+ try { parser.parse().iterate(); fail(); }
+ catch( Exception e ) { assertEquals("Redefining name 'i'",e.getMessage()); }
+ }
+
+
+ @Test
+ public void testVar14() {
+ Parser parser = new Parser("""
+struct B {};
+struct A { !B !b; };
+A x = new A {
+ return b; // read before init
+ b = new B;
+};
+""");
+ try { parser.parse().iterate(); fail(); }
+ catch( Exception e ) { assertEquals("Cannot read uninitialized field 'b'",e.getMessage()); }
+ }
+
+ @Test
+ public void testVar15() {
+ Parser parser = new Parser("""
+struct B {};
+struct A { !B !b; };
+return new A {
+ if (arg) b = new B; // Constructor ends with partial init of b
+}.b;
+""");
+ try { parser.parse().iterate(); fail(); }
+ catch( Exception e ) { assertEquals("'A' is not fully initialized, field 'b' needs to be set in a constructor",e.getMessage()); }
+ }
+
+ // ---------------------------------------------------------------
+ @Test
+ public void testTrinary0() {
+ Parser parser = new Parser("""
+return arg ? 1 : 2;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return Phi(Region,1,2);", stop.toString());
+ assertEquals(2L, Evaluator.evaluate(stop, 0));
+ assertEquals(1L, Evaluator.evaluate(stop, 1));
+ }
+
+ @Test
+ public void testTrinary1() {
+ Parser parser = new Parser("""
+return arg ? 0 : arg;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return 0;", stop.toString());
+ assertEquals(0L, Evaluator.evaluate(stop, 0));
+ }
+
+ @Test
+ public void testTrinary2() {
+ Parser parser = new Parser("""
+struct Bar { int x; };
+var b = arg ? new Bar : null;
+return b ? b.x++ + b.x++ : -1;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return Phi(Region,1,-1);", stop.toString());
+ assertEquals(-1L, Evaluator.evaluate(stop, 0));
+ assertEquals( 1L, Evaluator.evaluate(stop, 1));
+ }
+
+ @Test
+ public void testTrinary3() {
+ Parser parser = new Parser("""
+struct Bar { int x; };
+var b = arg ? new Bar;
+return b ? b.x++ + b.x++ : -1;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return Phi(Region,1,-1);", stop.toString());
+ assertEquals(-1L, Evaluator.evaluate(stop, 0));
+ assertEquals( 1L, Evaluator.evaluate(stop, 1));
+ }
+
+ @Test
+ public void testTrinary4() {
+ // This test case will benefit from an unzipping transformation
+ Parser parser = new Parser("""
+struct Bar { !Bar? !next; int x; };
+var b = arg ? new Bar { next = (arg==2) ? new Bar{x=2;}; x=1; };
+return b ? b.next ? b.next.x : b.x; // parses "b ? (b.next ? b.next.x : b.x) : 0"
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return Phi(Region,Phi(Region,.x,.x),0);", stop.toString());
+ assertEquals( 0L, Evaluator.evaluate(stop, 0));
+ assertEquals( 1L, Evaluator.evaluate(stop, 1));
+ assertEquals( 2L, Evaluator.evaluate(stop, 2));
+ }
+
+ @Test
+ public void testTrinary5() {
+ Parser parser = new Parser("""
+flt f=arg?1:1.2;
+return f; // missing widening
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return Phi(Region,1.0,1.2);", stop.toString());
+ assertEquals(1.2, Evaluator.evaluate(stop, 0));
+ }
+
+ // ---------------------------------------------------------------
+ @Test
+ public void testFor0() {
+ Parser parser = new Parser("""
+int sum=0;
+for( int i=0; i<arg; i++ )
+ sum += i;
+return sum;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", stop.toString());
+ assertEquals(3L, Evaluator.evaluate(stop, 3));
+ assertEquals(45L, Evaluator.evaluate(stop, 10));
+ }
+ @Test
+ public void testFor1() {
+ Parser parser = new Parser("""
+int sum=0, i=0;
+for( ; i<arg; i++ )
+ sum += i;
+return sum;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", stop.toString());
+ assertEquals(3L, Evaluator.evaluate(stop, 3));
+ assertEquals(45L, Evaluator.evaluate(stop, 10));
+ }
+ @Test
+ public void testFor2() {
+ Parser parser = new Parser("""
+int sum=0;
+for( int i=0; ; i++ ) {
+ if( i>=arg ) break;
+ sum += i;
+}
+return sum;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", stop.toString());
+ assertEquals(3L, Evaluator.evaluate(stop, 3));
+ assertEquals(45L, Evaluator.evaluate(stop, 10));
+ }
+ @Test
+ public void testFor3() {
+ Parser parser = new Parser("""
+int sum=0;
+for( int i=0; i<arg; )
+ sum += i++;
+return sum;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", stop.toString());
+ assertEquals(3L, Evaluator.evaluate(stop, 3));
+ assertEquals(45L, Evaluator.evaluate(stop, 10));
+ }
+ @Test
+ public void testFor4() {
+ Parser parser = new Parser("""
+int sum=0;
+for( int i=0; i<arg; i++ )
+ sum += i;
+return i;
+""");
+ try { parser.parse().iterate(); fail(); }
+ catch( Exception e ) { assertEquals("Undefined name 'i'",e.getMessage()); }
+ }
+ @Test
+ public void testFor5() {
+ Parser parser = new Parser("""
+for(;;arg++;) {}
+""");
+ try { parser.parse().iterate(); fail(); }
+ catch( Exception e ) { assertEquals("Syntax error, expected Unexpected code after expression: ;",e.getMessage()); }
+ }
+
+
+
+ // ---------------------------------------------------------------
+ @Test
+ public void testForward0() {
+ Parser parser = new Parser("""
+struct A{
+ !B? !f1;
+ !B? !f2;
+};
+return new A;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return A;", stop.toString());
+ assertEquals("Obj<A>{f1=null,f2=null}", Evaluator.evaluate(stop).toString());
+ }
+
+ @Test
+ public void testForward1() {
+ Parser parser = new Parser("""
+struct A{
+ !B?[]? !nil_array_of_b;
+ !B?[] not_array_of_b = new !B?[0];
+};
+return new A.not_array_of_b;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return [*B?];", stop.toString());
+ assertEquals("Obj<[*B?]>{#=0,[]=[]}", Evaluator.evaluate(stop).toString());
+ }
+
+ // ---------------------------------------------------------------
+ @Test
+ public void testLinkedList2() {
+ Parser parser = new Parser("""
+struct LLI { !LLI? !next; int i; };
+!LLI? !head = null;
+while( arg-- )
+ head = new LLI { next=head; i=arg; };
+int sum=0;
+var ptr = head; // A read-only ptr, to be assigned from read-only next fields
+for( ; ptr; ptr = ptr.next )
+ sum += ptr.i;
+return sum;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return Phi(Loop,0,(Phi_sum+.i));", stop.toString());
+ assertEquals(45L, Evaluator.evaluate(stop, 10));
+ }
+
+ @Test
+ public void sieveOfEratosthenes() {
+ Parser parser = new Parser(
+"""
+var ary = new bool[arg], primes = new int[arg];
+var nprimes=0, p=0;
+// Find primes while p^2 < arg
+for( p=2; p*p < arg; p++ ) {
+ // skip marked non-primes
+ while( ary[p] ) p++;
+ // p is now a prime
+ primes[nprimes++] = p;
+ // Mark out the rest non-primes
+ for( int i = p + p; i < ary#; i += p )
+ ary[i] = true;
+}
+// Now just collect the remaining primes, no more marking
+for( ; p < arg; p++ )
+ if( !ary[p] )
+ primes[nprimes++] = p;
+// Copy/shrink the result array
+var !rez = new int[nprimes];
+for( int j=0; j<nprimes; j++ )
+ rez[j] = primes[j];
+return rez;
+""");
+ StopNode stop = parser.parse().iterate();
+ assertEquals("return [int];", stop.toString());
+ Evaluator.Obj obj = (Evaluator.Obj)Evaluator.evaluate(stop, 20);
+ assertEquals("[int] {int #; int ![]; }",obj.struct().toString());
+ long nprimes = (Long)obj.fields()[0];
+ long[] primes = new long[]{2,3,5,7,11,13,17,19};
+ for( int i=0; i<nprimes; i++ )
+ assertEquals(primes[i],(long)(Long)obj.fields()[i+1]);
+ }
+
+}
diff --git a/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
new file mode 100644
index 00000000..0fa4283b
--- /dev/null
+++ b/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/src/test/java/com/seaofnodes/simple/MutabilityTest.java b/src/test/java/com/seaofnodes/simple/MutabilityTest.java
index 3442c54a..8f99a059 100644
--- a/src/test/java/com/seaofnodes/simple/MutabilityTest.java
+++ b/src/test/java/com/seaofnodes/simple/MutabilityTest.java
@@ -37,6 +37,12 @@ public class MutabilityTest {
bad("struct P { !P? next; int x; }; P p=new P{next=new P;}; p.next.x=7; return 0;");
}
+ @Test public void testInferencePreservesAccess() {
+ ok("struct P { int x; }; val p=new P; p.x=7; return p.x;", "7");
+ bad("struct P { int x; }; val p=new P; p=new P; return 0;");
+ bad("struct P { int x; }; P p=new P; var q=p; q.x=7; return 0;");
+ }
+
@Test public void testConstructorScope() {
ok("struct P { int ~x; }; P p=new P{if(arg) x=3; else x=7;}; return p.x;", "7");
bad("int ~outer=1; struct P { int x; }; P p=new P{outer=7;}; return outer;");