diff options
| author | Cliff Click <cliffc@acm.org> | 2025-02-07 09:12:14 -0800 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2025-02-07 09:12:14 -0800 |
| commit | 9d6e0fd1f8720f92164a3163d2f87a642300145d (patch) | |
| tree | 5b237cdf771ae6094501b70e007f869bfe656755 /chapter20/src | |
| parent | 4650aca029b98f92c1a3e66192c6f7735818ad74 (diff) | |
| download | Simple-9d6e0fd1f8720f92164a3163d2f87a642300145d.tar.gz Simple-9d6e0fd1f8720f92164a3163d2f87a642300145d.zip | |
Chapter20 initial push
Diffstat (limited to 'chapter20/src')
158 files changed, 20688 insertions, 0 deletions
diff --git a/chapter20/src/main/java/com/seaofnodes/simple/ASMPrinter.java b/chapter20/src/main/java/com/seaofnodes/simple/ASMPrinter.java new file mode 100644 index 00000000..5cc13210 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/ASMPrinter.java @@ -0,0 +1,140 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeRPC; + +public abstract class ASMPrinter { + + public static SB print(SB sb, CodeGen code) { + if( code._cfg==null ) + return sb.p("Need _cfg set, run after GCM"); + + // instruction address + int iadr = 0; + for( int i=0; i<code._cfg._len; i++ ) + if( code._cfg.at(i) instanceof FunNode fun ) + iadr = print(iadr,sb,code,fun,i); + return sb; + } + + private static int print(int iadr, SB sb, CodeGen code, FunNode fun, int cfgidx) { + // Function header + sb.nl().p("---"); + fun.sig().print(sb); + sb.p("---------------------------").nl(); + + + while( !(code._cfg.at(cfgidx) instanceof ReturnNode) ) + iadr = doBlock(iadr,sb,code,fun,cfgidx++); + + // Function separator + sb.p("---"); + fun.sig().print(sb); + sb.p("---------------------------").nl(); + return iadr; + } + + static int doBlock(int iadr, SB sb, CodeGen code, FunNode fun, int cfgidx) { + CFGNode bb = code._cfg.at(cfgidx); + if( bb != fun && !(bb instanceof IfNode) && !(bb instanceof CallEndNode) && !(bb instanceof CallNode) && !(bb instanceof CProjNode && bb.in(0) instanceof CallEndNode )) + sb.p("L").p(bb._nid).p(":").nl(); + + if( !(bb instanceof CallNode) ) + for( Node n : bb._outputs ) + iadr = doInst(iadr, sb,code,fun,bb,n); + + return iadr; + } + + static int doInst(int iadr, SB sb, CodeGen code, FunNode fun, CFGNode bb, Node n) { + final int encWidth = 8; + final int opWidth = 5; + final int argWidth = 30; + + if( n instanceof CProjNode ) return iadr; + + // Phis are useful before (and during) reg alloc + if( n instanceof PhiNode phi ) { + if( phi._type instanceof TypeMem || phi._type instanceof TypeRPC ) return iadr; // Nothing for the hidden ones + sb.fix(4," ").p(" ").fix(encWidth,"").p(" ").fix(opWidth,phi._label).p(" "); + sb.p(code.reg(n)); + if( !(n instanceof ParmNode) ) { + sb.p(" = phi( "); + for( int i=1; i<n.nIns(); i++ ) + sb.p("N").p(n.in(i)._nid).p(","); + sb.unchar().p(" )"); + } + sb.nl(); + return iadr; + } + + // All blocks ending in a Region will need to either fall into or jump + // to this block. Until the post-reg-alloc block layout cleanup, we + // need to assume a jump. There's no real hardware op here, yet. + if( n instanceof RegionNode && !(n instanceof FunNode) ) { + sb.hex4(iadr++).p(" ").fix(encWidth,"??").p(" ").fix(opWidth,"JMP").p(" ").fix(argWidth,"L"+n._nid).nl(); + return iadr; + } + + // ProjNodes following a multi (e.g. Call or New results), + // get indent slightly and just print their index & node# + if( n instanceof ProjNode proj ) { + if( proj._type instanceof TypeMem ) return iadr; // Nothing for the hidden ones + sb.fix(4," ").p(" ").fix(encWidth,"").p(" ").fix(opWidth,proj._label==null ? "---" : proj._label).p(" ").p(code.reg(n)).nl(); + return iadr; + } + + // ADDR ENCODING Op--- dst = src op src // Comment + // 1234 abcdefgh ld4 RAX = [Rbase + off] // Comment + sb.hex4(iadr); + sb.p(" "); + + // Encoding + int size = 1; // TODO: Fake encoding size + iadr += size; + sb.fix(encWidth,"??"); + sb.p(" "); + + // Op; generally "ld4" or "call" + sb.fix(opWidth, n instanceof MachNode mach ? mach.op() : n.label()); + sb.p(" "); + + // General asm args + if( n instanceof MachNode mach ) { + int old = sb.len(); + mach.asm(code,sb); + sb.fix(argWidth-(sb.len()-old),""); // Pad out + + } else if( !(n._type instanceof TypeMem) ) { + // Room for some inputs + sb.fix(5, n._nid+":" ); + int off = 0; + for( int i=0; i<n.nIns(); i++ ) { + sb.fix(5, n.in(i) == null ? "___" : String.valueOf(n.in(i)._nid)); + off += 5; + if( off >= argWidth ) break; + } + if( off < argWidth ) sb.fix(argWidth-off,""); + } + sb.p(" "); + + // Comment + String comment = n.comment(); + if( comment!=null ) sb.p("// ").p(comment); + + sb.nl(); + + // MultiNodes are immediately followed by projection(s) + if( !(n instanceof CFGNode) && n instanceof MultiNode ) { + for( Node proj : n._outputs ) { + assert proj instanceof ProjNode; + doInst(iadr,sb,code,fun,bb,proj); + } + } + + return iadr; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/Ary.java b/chapter20/src/main/java/com/seaofnodes/simple/Ary.java new file mode 100644 index 00000000..73ada8e0 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/Ary.java @@ -0,0 +1,217 @@ +package com.seaofnodes.simple; + +import java.lang.StringBuilder; +import java.lang.reflect.Array; +import java.util.*; + +// ArrayList with saner syntax +public class Ary<E> extends AbstractList<E> implements List<E> { + public E[] _es; + public int _len; + + public Ary(E[] es) { this(es,es.length); } + public Ary(E[] es, int len) { if( es.length==0 ) es=Arrays.copyOf(es,1); _es=es; _len=len; } + @SuppressWarnings("unchecked") + public Ary(Class<E> clazz) { this((E[]) Array.newInstance(clazz, 1),0); } + + /** @return list is empty */ + public boolean isEmpty() { return _len==0; } + /** @return active list length */ + public int len() { return _len; } + public int size() { return len(); } + + /** @param i element index + * @return element being returned; throws if OOB + * @exception AIOOBE if !(0 <= i < _len) + */ + public E get( int i ) { return at(i); } + public E at( int i ) { + range_check(i); + return _es[i]; + } + + /** @return last element */ + public E last( ) { return at(_len-1); } + + /** Add element in amortized constant time + * @param e Element to add at end of list + **/ + public E push( E e ) { + if( _len >= _es.length ) _es = Arrays.copyOf(_es,Math.max(1,_es.length<<1)); + return (_es[_len++] = e); + } + /** @return remove and return last element */ + public E pop( ) { + return _len==0 ? null : _es[--_len]; + } + @Override public E removeLast() { return pop(); } + + + /** Add element in amortized constant time + * @param e Element to add at end of list + * @return true */ + public boolean add( E e ) { + if( _len >= _es.length ) _es = Arrays.copyOf(_es,Math.max(1,_es.length<<1)); + _es[_len++] = e; + return true; + } + + /** Fast, constant-time, element removal. Does not preserve order + * @param i element to be removed + * @return element removed */ + public E del( int i ) { + range_check(i); + E tmp = _es[i]; + _es[i]=_es[--_len]; + return tmp; + } + + /** Slow, linear-time, element removal. Preserves order. + * @param i element to be removed + * @return element removed */ + @Override public E remove( int i ) { + range_check(i); + E e = _es[i]; + System.arraycopy(_es,i+1,_es,i,(--_len)-i); + return e; + } + + /** Remove all elements */ + public void clear( ) { _len=0; } + + /** Set existing element + * @param i element to set + * @param e value to set + * @return old value + * @exception AIOOBE if !(0 <= i < _len) + */ + public E set( int i, E e ) { + range_check(i); + E old = _es[i]; + _es[i] = e; + return old; + } + + /** Set existing element + * @param i element to set + * @param e value to set + * @return old value + * @exception AIOOBE if !(0 <= i < _len) + */ + public E setX( int i, E e ) { + if( i >= _len ) { + while( i >= _es.length ) _es = Arrays.copyOf( _es, _es.length << 1 ); + _len = i+1; + } + E old = _es[i]; + _es[i] = e; + return old; + } + + public Ary<E> setLen( int len ) { + if( len > _len ) + while( len>= _es.length ) _es = Arrays.copyOf(_es,_es.length<<1); + _len = len; + while( _es.length > (len<<1) ) // Shrink if hugely too large + _es = Arrays.copyOf(_es,_es.length>>1); + Arrays.fill(_es,len,_es.length,null); + return this; + } + + public E swap( int i, int j ) { + range_check(i); + range_check(j); + E tmp = _es[i]; + if( i==j ) return tmp; + _es[i] = _es[j]; + _es[j] = tmp; + return tmp; + } + + + /** @param c Collection to be added */ + public boolean addAll( Collection<? extends E> c ) { + if( c==null ) return false; + for( E e : c ) add(e); + return true; + } + public Ary<E> addAll( Iterable<? extends E> it ) { for( E e : it ) add(e); return this; } + + /** @param es Array to be added */ + public <F extends E> Ary<E> addAll( F[] es ) { + if( es==null || es.length==0 ) return this; + while( _len+es.length > _es.length ) _es = Arrays.copyOf(_es,_es.length<<1); + System.arraycopy(es,0,_es,_len,es.length); + _len += es.length; + return this; + } + + /** @param c Collection to be added */ + public Ary<E> addAll( Ary<? extends E> c ) { + if( c==null || c._len==0 ) return this; + while( _len+c._len > _es.length ) _es = Arrays.copyOf(_es,_es.length<<1); + System.arraycopy(c._es,0,_es,_len,c._len); + _len += c._len; + return this; + } + + /** @return compact array version */ + public E[] asAry() { return Arrays.copyOf(_es,_len); } + + /** Sorts in-place + * @param c Comparator to sort by */ + public void sort_update(Comparator<? super E> c ) { Arrays.sort(_es, 0, _len, c); } + + /** Find the first matching element using ==, or -1 if none. Note that + * most del calls shuffle the list, so the first element might be random. + * @param e Element to find + * @return index of first matching element, or -1 if none */ + public int find( E e ) { + for( int i=0; i<_len; i++ ) if( _es[i]==e ) return i; + return -1; + } + + + /** @return an iterator */ + @Override public Iterator<E> iterator() { return new Iter(); } + private class Iter implements Iterator<E> { + int _i=0; + @Override public boolean hasNext() { return _i<_len; } + @Override public E next() { return _es[_i++]; } + } + + @Override public String toString() { + StringBuilder sb = new StringBuilder().append('{'); + for( int i=0; i<_len; i++ ) { + if( i>0 ) sb.append(','); + if( _es[i] != null ) sb.append(_es[i].toString()); + } + return sb.append('}').toString(); + } + + private void range_check( int i ) { + if( i < 0 || i>=_len ) + throw new ArrayIndexOutOfBoundsException(""+i+" >= "+_len); + } + + // Note that the hashCode() and equals() are not invariant to changes in the + // underlying array. If the hashCode() is used (e.g., inserting into a + // HashMap) and the then the array changes, the hashCode() will change also. + @Override public boolean equals( Object o ) { + if( this==o ) return true; + if( !(o instanceof Ary ary) ) return false; + if( _len != ary._len ) return false; + if( _es == ary._es ) return true; + for( int i=0; i<_len; i++ ) + if( !(_es[i]==null ? (ary._es[i] == null) : _es[i].equals(ary._es[i])) ) + return false; + return true; + } + @Override public int hashCode( ) { + int sum=_len; + for( int i=0; i<_len; i++ ) + sum += _es[i]==null ? 0 : _es[i].hashCode(); + return sum; + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/CodeGen.java b/chapter20/src/main/java/com/seaofnodes/simple/CodeGen.java new file mode 100644 index 00000000..c48a4162 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/CodeGen.java @@ -0,0 +1,288 @@ + package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.cpus.x86_64_v2.MemOpX86; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class CodeGen { + // Last created CodeGen as a global; used all over to avoid passing about a + // "context". + public static CodeGen CODE; + + public enum Phase { + Parse, // Parse ASCII text into Sea-of-Nodes IR + Opto, // Run ideal optimizations + TypeCheck, // Last check for bad programs + InstructionSelection, // Convert to target hardware nodes + Schedule, // Global schedule (code motion) nodes + LocalSched, // Local schedule + RegAlloc; // Register allocation + } + public enum CallingConv { + Win64, + SystemV + } + public Phase _phase; + + // --------------------------- + // Compilation source code + public final String _src; + // Compile-time known initial argument type + public final TypeInteger _arg; + public CallingConv _callingConv; + + // --------------------------- + public CodeGen( String src ) { this(src, TypeInteger.BOT, 123L ); } + public CodeGen( String src, TypeInteger arg, long workListSeed ) { + CODE = this; + _phase = null; + _callingConv = null; + _start = new StartNode(arg); + _stop = new StopNode(src); + _src = src; + _arg = arg; + _iter = new IterPeeps(workListSeed); + P = new Parser(this,arg); + } + + + // --------------------------- + /** + * A counter, for unique node id generation. Starting with value 1, to + * avoid bugs confusing node ID 0 with uninitialized values. + */ + private int _uid = 1; + public int UID() { return _uid; } + public int getUID() { return _uid++; } + + // Next available memory alias number + private int _alias = 2; // 0 is for control, 1 for memory + public int getALIAS() { return _alias++; } + + + // Popular visit bitset, declared here, so it gets reused all over + public final BitSet _visit = new BitSet(); + public BitSet visit() { assert _visit.isEmpty(); return _visit; } + + // Start and stop; end points of the generated IR + public StartNode _start; + public StopNode _stop; + + // Global Value Numbering. Hash over opcode and inputs; hits in this table + // are structurally equal. + public final HashMap<Node,Node> _gvn = new HashMap<>(); + + // Compute "function indices": FIDX. + // Each new request at the same signature gets a new FIDX. + private final HashMap<TypeTuple,Integer> FIDXS = new HashMap<>(); + public TypeFunPtr makeFun( TypeTuple sig, Type ret ) { + Integer i = FIDXS.get(sig); + int fidx = i==null ? 0 : i; + FIDXS.put(sig,fidx+1); // Track count per sig + assert fidx<64; // TODO: need a larger FIDX space + return TypeFunPtr.make((byte)2,sig,ret, 1L<<fidx ); + } + // Signature for MAIN + public TypeFunPtr _main = makeFun(TypeTuple.MAIN,Type.BOTTOM); + // Reverse from a constant function pointer to the IR function being called + public FunNode link( TypeFunPtr tfp ) { + assert tfp.isConstant(); + return _linker.get(tfp.makeFrom(Type.BOTTOM)); + } + + // Insert linker mapping from constant function signature to the function + // being called. + public void link(FunNode fun) { + _linker.put(fun.sig().makeFrom(Type.BOTTOM),fun); + } + + // "Linker" mapping from constant TypeFunPtrs to heads of function. These + // TFPs all have exact single fidxs and their return is wiped to BOTTOM (so + // the return is not part of the match). + private final HashMap<TypeFunPtr,FunNode> _linker = new HashMap<>(); + + // Parser object + public final Parser P; + + // Parse ASCII text into Sea-of-Nodes IR + public CodeGen parse() { + assert _phase == null; + _phase = Phase.Parse; + + P.parse(); + JSViewer.show(); + return this; + } + + + // --------------------------- + // Iterator peepholes. + public final IterPeeps _iter; + // Stop tracking deps while assertin + public boolean _midAssert; + // Statistics on peepholes + public int _iter_cnt, _iter_nop_cnt; + // Stat gathering + public void iterCnt() { if( !_midAssert ) _iter_cnt++; } + public void iterNop() { if( !_midAssert ) _iter_nop_cnt++; } + + // Run ideal optimizations + public CodeGen opto() { + assert _phase == Phase.Parse; + _phase = Phase.Opto; + + // Pessimistic peephole optimization on a worklist + _iter.iterate(this); + + // TODO: + // Optimistic + // TODO: + // loop unroll, peel, RCE, etc + return this; + } + public <N extends Node> N add( N n ) { return (N)_iter.add(n); } + public void addAll( Ary<Node> ary ) { _iter.addAll(ary); } + + + // --------------------------- + // Last check for bad programs + public CodeGen typeCheck() { + // Demand phase Opto for cleaning up dead control flow at least, + // required for the following GCM. + assert _phase.ordinal() <= Phase.Opto.ordinal(); + _phase = Phase.TypeCheck; + + Parser.ParseException err = _stop.walk( Node::err ); + if( err != null ) + throw err; + return this; + } + + + // --------------------------- + // Code generation CPU target + public Machine _mach; + + // Convert to target hardware nodes + public CodeGen instSelect( String cpu, String callingConv ) { + assert _phase.ordinal() <= Phase.TypeCheck.ordinal(); + _phase = Phase.InstructionSelection; + + _callingConv = switch(callingConv) { + case "SystemV" -> CallingConv.SystemV; + case "Win64" -> CallingConv.Win64; + default -> throw Utils.TODO(); // Unknown calling convention + }; + + // Look for CPU in fixed named place: + // com.seaofnodes.simple.node.cpus."cpu"."cpu.class" + String clzFile = "com.seaofnodes.simple.node.cpus."+cpu+"."+cpu; + try { _mach = ((Class<Machine>) Class.forName( clzFile )).getDeclaredConstructor().newInstance(); } + catch( Exception e ) { throw new RuntimeException(e); } + + // Convert to machine ops + _uid = 1; // All new machine nodes reset numbering + var map = new IdentityHashMap<Node,Node>(); + _instSelect( _stop, map ); + _stop = ( StopNode)map.get(_stop ); + _start = (StartNode)map.get(_start); + _instOuts(_stop,visit()); + _visit.clear(); + return this; + } + + // Walk all ideal nodes, recursively mapping ideal to machine nodes, then + // make a machine node for "this". + private Node _instSelect( Node n, IdentityHashMap<Node,Node> map ) { + if( n==null ) return null; + Node x = map.get(n); + if( x !=null ) return x; // Been there, done that + + // If n is a MachConcrete, then its part of a multi-node expansion. + // It does not need instruction selection (already selected!) + // but it does need its inputs walked. + if( n instanceof MachConcreteNode || n instanceof MemOpX86 ) { + for( int i=0; i < n.nIns(); i++ ) + n._inputs.set(i, _instSelect(n.in(i),map) ); + return n; + } + + // Produce a machine node from n; map it to flag as done so stops cycles. + map.put(n, x=_mach.instSelect(n) ); + // Walk machine op and replace inputs with mapped inputs + for( int i=0; i < x.nIns(); i++ ) + x._inputs.set(i, _instSelect(x.in(i),map) ); + if( x instanceof MachNode mach ) + mach.postSelect(); // Post selection action + + // Updates forward edges only. + n._outputs.clear(); + return x; + } + + // Walk all machine Nodes, and set their output edges + private void _instOuts( Node n, BitSet visit ) { + if( visit.get(n._nid) ) return; + visit.set(n._nid); + for( Node in : n._inputs ) + if( in!=null ) { + in._outputs.push(n); + _instOuts(in,visit); + } + } + + + // --------------------------- + // Control Flow Graph in RPO order. + Ary<CFGNode> _cfg = new Ary<>(CFGNode.class); + + // Global schedule (code motion) nodes + public CodeGen GCM() { return GCM(false); } + public CodeGen GCM( boolean show) { + assert _phase.ordinal() <= Phase.InstructionSelection.ordinal(); + _phase = Phase.Schedule; + + // Build the loop tree, fix never-exit loops + _start.buildLoopTree(_stop); + GlobalCodeMotion.buildCFG(this); + if( show ) + System.out.println(new GraphVisualizer().generateDotOutput(_stop,null,null)); + return this; + } + + // --------------------------- + // Local (basic block) scheduler phase, a classic list scheduler + public CodeGen localSched() { + assert _phase == Phase.Schedule; + _phase = Phase.LocalSched; + ListScheduler.sched(this); + return this; + } + + + // --------------------------- + public String reg(Node n) { + if( _phase == Phase.RegAlloc ) throw Utils.TODO(); + return "N"+ n._nid; + } + + + // --------------------------- + SB asm(SB sb) { return ASMPrinter.print(sb,this); } + String asm() { return asm(new SB()).toString(); } + + + // Testing shortcuts + Node ctrl() { return _stop.ret().ctrl(); } + Node expr() { return _stop.ret().expr(); } + String print() { return _stop.print(); } + + // Debugging helper + @Override public String toString() { return _stop.p(9999); } + + // Debugging helper + public Node f(int idx) { return _stop.find(idx); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java b/chapter20/src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java new file mode 100644 index 00000000..12dd9243 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/GlobalCodeMotion.java @@ -0,0 +1,241 @@ + package com.seaofnodes.simple; + +import com.seaofnodes.simple.IterPeeps.WorkList; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import java.util.*; + +public abstract class GlobalCodeMotion { + + // Arrange that the existing isCFG() Nodes form a valid CFG. The + // Node.use(0) is always a block tail (either IfNode or head of the + // following block). There are no unreachable infinite loops. + public static void buildCFG( CodeGen code ) { + Ary<CFGNode> rpo = new Ary<>(CFGNode.class); + _rpo_cfg(null, code._start, code.visit(), rpo); + // Reverse in-place + for( int i=0; i< rpo.size()>>1; i++ ) { + int j = rpo.size()-1-i; + CFGNode tmp = rpo.get(i); + rpo.set(i, rpo.get(j)); + rpo.set(j, tmp); + } + // Set global CFG + code._cfg = rpo; + + schedEarly(code); + code._visit.clear(); + schedLate (code); + } + + // Post-Order of CFG + private static void _rpo_cfg(CFGNode def, Node use, BitSet visit, Ary<CFGNode> rpo) { + if( !(use instanceof CFGNode cfg) || visit.get(cfg._nid) ) + return; // Been there, done that + if( def instanceof CallNode && cfg instanceof FunNode ) + return; // Ignore linked function calls + visit.set(cfg._nid); + for( Node useuse : cfg._outputs ) + _rpo_cfg(cfg,useuse,visit,rpo); + rpo.add(cfg); + } + + // ------------------------------------------------------------------------ + // Visit all nodes in CFG Reverse Post-Order, essentially defs before uses + // (except at loops). Since defs are visited first - and hoisted as early + // as possible, when we come to a use we place it just after its deepest + // input. + private static void schedEarly(CodeGen code) { + // Reverse Post-Order on CFG + for( CFGNode cfg : code._cfg ) { + cfg.loopDepth(); + for( Node n : cfg._inputs ) + _schedEarly(n,code._visit ); + if( cfg instanceof RegionNode ) + for( Node phi : cfg._outputs ) + if( phi instanceof PhiNode ) + _schedEarly(phi,code._visit ); + } + } + + private static void _schedEarly(Node n, BitSet visit) { + if( n==null || visit.get(n._nid) ) return; // Been there, done that + assert !(n instanceof CFGNode); + visit.set(n._nid); + // Schedule not-pinned not-CFG inputs before self. Since skipping + // Pinned, this never walks the backedge of Phis (and thus spins around + // a data-only loop), eventually attempting relying on some pre-visited- + // not-post-visited data op with no scheduled control. + for( Node def : n._inputs ) + if( def!=null && !(def instanceof PhiNode) ) + _schedEarly(def,visit); + + // If not-pinned (e.g. constants, projections, phi) and not-CFG + if( !n.isPinned() ) { + // Schedule at deepest input + CFGNode early = CodeGen.CODE._start; // Maximally early, lowest idepth + if( n.in(0) instanceof CFGNode cfg ) early = cfg; + for( int i=1; i<n.nIns(); i++ ) + if( n.in(i)!=null && n.in(i).cfg0().idepth() > early.idepth() ) + early = n.in(i).cfg0(); // Latest/deepest input + n.setDef(0,early); + } + } + + // ------------------------------------------------------------------------ + private static void schedLate( CodeGen code ) { + CFGNode[] late = new CFGNode[code.UID()]; + Node[] ns = new Node[code.UID()]; + // Breadth-first scheduling + breadth(code._stop,ns,late); + + // Copy the best placement choice into the control slot + for( int i=0; i<late.length; i++ ) + if( ns[i] != null && !(ns[i] instanceof ProjNode) ) + ns[i].setDef(0,late[i]); + } + + private static void breadth(Node stop, Node[] ns, CFGNode[] late) { + // Things on the worklist have some (but perhaps not all) uses done. + WorkList<Node> work = new WorkList<>(); + work.push(stop); + Node n; + outer: + while( (n = work.pop()) != null ) { + assert late[n._nid]==null; // No double visit + // These I know the late schedule of, and need to set early for loops + if( n instanceof CFGNode cfg ) late[n._nid] = cfg.blockHead() ? cfg : cfg.cfg(0); + else if( n instanceof PhiNode phi ) late[n._nid] = phi.region(); + else if( n instanceof ProjNode && n.in(0) instanceof CFGNode cfg ) late[n._nid] = cfg; + else { + + // All uses done? + for( Node use : n._outputs ) + if( use!=null && late[use._nid]==null ) + continue outer; // Nope, await all uses done + + // Loads need their memory inputs' uses also done + if( n instanceof LoadNode ld ) + for( Node memuse : ld.mem()._outputs ) + if( late[memuse._nid]==null && + // New makes new memory, never crushes load memory + !(memuse instanceof NewNode) && + // Load-use directly defines memory + (memuse._type instanceof TypeMem || + // Load-use indirectly defines memory + (memuse._type instanceof TypeTuple tt && tt._types[ld._alias] instanceof TypeMem)) ) + continue outer; + + // All uses done, schedule + _doSchedLate(n,ns,late); + } + + // Walk all inputs and put on worklist, as their last-use might now be done + for( Node def : n._inputs ) + if( def!=null && late[def._nid]==null ) { + work.push(def); + // if the def has a load use, maybe the load can fire + for( Node ld : def._outputs ) + if( ld instanceof LoadNode && late[ld._nid]==null ) + work.push(ld); + } + } + } + + private static void _doSchedLate(Node n, Node[] ns, CFGNode[] late) { + // Walk uses, gathering the LCA (Least Common Ancestor) of uses + CFGNode early = n.in(0) instanceof CFGNode cfg ? cfg : n.in(0).cfg0(); + assert early != null; + CFGNode lca = null; + for( Node use : n._outputs ) + if( use != null ) + lca = use_block(n,use, late)._idom(lca,null); + + // Loads may need anti-dependencies, raising their LCA + if( n instanceof LoadNode load ) + lca = find_anti_dep(lca,load,early,late); + + // Walk up from the LCA to the early, looking for best place. This is + // the lowest execution frequency, approximated by least loop depth and + // deepest control flow. + CFGNode best = lca; + lca = lca.idom(); // Already found best for starting LCA + for( ; lca != early.idom(); lca = lca.idom() ) + if( better(lca,best) ) + best = lca; + assert !(best instanceof IfNode); + ns [n._nid] = n; + late[n._nid] = best; + } + + // Block of use. Normally from late[] schedule, except for Phis, which go + // to the matching Region input. + private static CFGNode use_block(Node n, Node use, CFGNode[] late) { + if( !(use instanceof PhiNode phi) ) + return late[use._nid]; + CFGNode found=null; + for( int i=1; i<phi.nIns(); i++ ) + if( phi.in(i)==n ) + found = phi.region().cfg(i)._idom(found,null); + + assert found!=null; + return found; + } + + + // Least loop depth first, then largest idepth + private static boolean better( CFGNode lca, CFGNode best ) { + return lca.loopDepth() < best.loopDepth() || + (lca.idepth() > best.idepth() || best instanceof IfNode); + } + + private static CFGNode find_anti_dep(CFGNode lca, LoadNode load, CFGNode early, CFGNode[] late) { + // We could skip final-field loads here. + // Walk LCA->early, flagging Load's block location choices + for( CFGNode cfg=lca; early!=null && cfg!=early.idom(); cfg = cfg.idom() ) + cfg._anti = load._nid; + // Walk load->mem uses, looking for Stores causing an anti-dep + for( Node mem : load.mem()._outputs ) { + switch( mem ) { + case StoreNode st: + assert late[st._nid]!=null; + lca = anti_dep(load,late[st._nid],st.cfg0(),lca,st); + break; + case CallNode st: + assert late[st._nid]!=null; + lca = anti_dep(load,late[st._nid],st.cfg0(),lca,st); + break; + case PhiNode phi: + // Repeat anti-dep for matching Phi inputs. + // No anti-dep edges but may raise the LCA. + for( int i=1; i<phi.nIns(); i++ ) + if( phi.in(i)==load.mem() ) + lca = anti_dep(load,phi.region().cfg(i),load.mem().cfg0(),lca,null); + break; + case NewNode st: break; + case LoadNode ld: break; // Loads do not cause anti-deps on other loads + case ReturnNode ret: break; // Load must already be ahead of Return + case MemMergeNode ret: break; // Mem uses now on ScopeMin + case NeverNode never: break; + default: throw Utils.TODO(); + } + } + return lca; + } + + // + private static CFGNode anti_dep( LoadNode load, CFGNode stblk, CFGNode defblk, CFGNode lca, Node st ) { + // Walk store blocks "reach" from its scheduled location to its earliest + for( ; stblk != defblk.idom(); stblk = stblk.idom() ) { + // Store and Load overlap, need anti-dependence + if( stblk._anti==load._nid ) { + lca = stblk._idom(lca,null); // Raise Loads LCA + if( lca == stblk && st != null && st._inputs.find(load) == -1 ) // And if something moved, + st.addDef(load); // Add anti-dep as well + return lca; // Cap this stores' anti-dep to here + } + } + return lca; + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/GraphVisualizer.java b/chapter20/src/main/java/com/seaofnodes/simple/GraphVisualizer.java new file mode 100644 index 00000000..30e25916 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/GraphVisualizer.java @@ -0,0 +1,288 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; + +import java.util.*; + +import static com.seaofnodes.simple.Utils.TODO; + +/** + * Simple visualizer that outputs GraphViz dot format. + * The dot output must be saved to a file and run manually via dot to generate the SVG output. + * Currently, this is done manually. + */ +public class GraphVisualizer { + + /** + * If set to true we put the control nodes in a separate cluster from + * data nodes. + */ + boolean _separateControlCluster = false; + + public GraphVisualizer(boolean separateControlCluster) { this._separateControlCluster = separateControlCluster; } + public GraphVisualizer() { this(false); } + + public String generateDotOutput(Parser parse) { return generateDotOutput(parse._code._stop,parse._scope,parse._xScopes); } + public String generateDotOutput(StopNode stop, Node scope, Stack<ScopeNode> xScopes) { + + // Since the graph has cycles, we need to create a flat list of all the + // nodes in the graph. + Collection<Node> all = findAll(xScopes, stop, scope); + StringBuilder sb = new StringBuilder(); + sb.append("digraph chapter18 {\n"); + sb.append("/*\n"); + sb.append(stop._src); + sb.append("\n*/\n"); + + // To keep the Scopes below the graph and pointing up into the graph we + // need to group the Nodes in a subgraph cluster, and the scopes into a + // different subgraph cluster. THEN we can draw edges between the + // scopes and nodes. If we try to cross subgraph cluster borders while + // still making the subgraphs DOT gets confused. + sb.append("\trankdir=BT;\n"); // Force Nodes before Scopes + + // CNC - turned off Apr/8/2024, gives more flex in the layout and + // removes some of the more ludicrous layout choices. + // Preserve node input order + //sb.append("\tordering=\"in\";\n"); + + // Merge multiple edges hitting the same node. Makes common shared + // nodes much prettier to look at. + sb.append("\tconcentrate=\"true\";\n"); + + // Force nested scopes to order + sb.append("\tcompound=\"true\";\n"); + + // Just the Nodes first, in a cluster no edges + nodes(sb, all); + + // Now the scopes, in a cluster no edges + if( xScopes != null ) + for( ScopeNode xscope : xScopes ) + if( !xscope.isDead() ) + scope( sb, xscope ); + + // Walk the Node edges + nodeEdges(sb, all); + + // Walk the active Scope edges + if( xScopes != null ) + for( ScopeNode xscope : xScopes ) + if( !xscope.isDead() ) + scopeEdges( sb, xscope ); + + sb.append("}\n"); + return sb.toString(); + } + + private void nodesByCluster(StringBuilder sb, boolean doCtrl, Collection<Node> all) { + if (!_separateControlCluster && doCtrl) // all nodes in 1 cluster + return; + // Just the Nodes first, in a cluster no edges + sb.append(doCtrl ? "\tsubgraph cluster_Controls {\n" : "\tsubgraph cluster_Nodes {\n"); // Magic "cluster_" in the subgraph name + for (Node n : all) { + if (n instanceof ProjNode || n instanceof CProjNode || n instanceof MemMergeNode || n==Parser.XCTRL) + continue; // Do not emit, rolled into MultiNode or Scope cluster already + if( _separateControlCluster && doCtrl && !(n instanceof CFGNode) ) continue; + if( _separateControlCluster && !doCtrl && (n instanceof CFGNode) ) continue; + sb.append("\t\t").append(n.uniqueName()).append(" [ "); + String lab = n.glabel(); + if (n instanceof MultiNode) { + // Make a box with the MultiNode on top, and all the projections on the bottom + sb.append("shape=plaintext label=<\n"); + sb.append("\t\t\t<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"4\">\n"); + sb.append("\t\t\t<TR><TD BGCOLOR=\"yellow\">").append(lab).append("</TD></TR>\n"); + sb.append("\t\t\t<TR>"); + boolean doProjTable = false; + n._outputs.sort((x,y) -> x instanceof ProjNode xp && y instanceof ProjNode yp ? (xp._idx - yp._idx) : (x._nid - y._nid)); + for (Node use : n._outputs) { + if (use instanceof ProjNode proj) { + if (!doProjTable) { + doProjTable = true; + sb.append("<TD>").append("\n"); + sb.append("\t\t\t\t<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">").append("\n"); + sb.append("\t\t\t\t<TR>"); + } + sb.append("<TD PORT=\"p").append(proj._idx).append("\">").append(proj.glabel()).append("</TD>"); + } + if (use instanceof CProjNode proj) { + if (!doProjTable) { + doProjTable = true; + sb.append("<TD>").append("\n"); + sb.append("\t\t\t\t<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">").append("\n"); + sb.append("\t\t\t\t<TR>"); + } + sb.append("<TD PORT=\"p").append(proj._idx).append("\" BGCOLOR=\"yellow\">").append(proj.glabel()).append("</TD>"); + } + } + if (doProjTable) { + sb.append("</TR>").append("\n"); + sb.append("\t\t\t\t</TABLE>").append("\n"); + sb.append("\t\t\t</TD>"); + } + sb.append("</TR>\n"); + sb.append("\t\t\t</TABLE>>\n\t\t"); + + } else { + // control nodes have box shape + // other nodes are ellipses, i.e. default shape + if( n instanceof CFGNode ) sb.append("shape=box style=filled fillcolor=yellow "); + else if (n instanceof PhiNode) sb.append("style=filled fillcolor=lightyellow "); + sb.append("label=\"").append(lab).append("\" "); + } + sb.append("];\n"); + } + if (!_separateControlCluster) { + // Force Region & Phis to line up + for (Node n : all) { + if (n instanceof RegionNode region) { + sb.append("\t\t{ rank=same; "); + sb.append(region.uniqueName()).append(";"); + for (Node phi : region._outputs) + if (phi instanceof PhiNode) sb.append(phi.uniqueName()).append(";"); + sb.append("}\n"); + } + } + } + sb.append("\t}\n"); // End Node cluster + } + + private void nodes(StringBuilder sb, Collection<Node> all) { + nodesByCluster(sb, true, all); + nodesByCluster(sb, false, all); + } + + // Build a nested scope display, walking the _prev edge + private void scope( StringBuilder sb, ScopeNode scope ) { + sb.append("\tnode [shape=plaintext];\n"); + int last = scope.nIns(); + int max = scope._lexSize.size(); + for( int i = 0; i < max; i++ ) { + int level = max-i-1; + String scopeName = makeScopeName(scope, level); + sb.append("\tsubgraph cluster_").append(scopeName).append(" {\n"); // Magic "cluster_" in the subgraph name + // Special for memory ScopeMinNode + if( level==0 ) { + MemMergeNode n = scope.mem(); + sb.append("\t\t").append(n.uniqueName()).append(" [label=\"").append(n.glabel()).append("\"];\n"); + } + sb.append("\t\t").append(scopeName).append(" [label=<\n"); + sb.append("\t\t\t<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">\n"); + // Add the scope level + sb.append("\t\t\t<TR><TD BGCOLOR=\"cyan\">").append(level).append("</TD>"); + int lexStart=scope._lexSize.at(level); + for( int j=lexStart; j<last; j++ ) { + var v = scope._vars.at(j); + sb.append("<TD PORT=\"").append(makePortName(scopeName, v._name)).append("\">").append(v._name).append("</TD>"); + } + last = lexStart; + sb.append("</TR>\n"); + sb.append("\t\t\t</TABLE>>];\n"); + } + // Scope clusters nest, so the graphics shows the nested scopes, so + // they are not closed as they are printed; so they just keep nesting. + // We close them all at once here. + sb.append( "\t}\n".repeat( max ) ); // End all Scope clusters + } + + private String makeScopeName(ScopeNode sn, int level) { return sn.uniqueName() + "_" + level; } + private String makePortName(String scopeName, String varName) { return scopeName + "_" + varName; } + + // Walk the node edges + private void nodeEdges(StringBuilder sb, Collection<Node> all) { + // All them edge labels + sb.append("\tedge [ fontname=Helvetica, fontsize=8 ];\n"); + for( Node n : all ) { + // Do not display the Constant->Start edge; + // ProjNodes handled by Multi; + // ScopeNodes are done separately + if( n instanceof ConstantNode || n instanceof ProjNode || n instanceof CProjNode || n instanceof ScopeNode || n==Parser.XCTRL ) + continue; + for( int i=0; i<n.nIns(); i++ ) { + Node def = n.in(i); + if( n instanceof PhiNode && def instanceof RegionNode ) { + // Draw a dotted use->def edge from Phi to Region. + sb.append('\t').append(n.uniqueName()); + sb.append(" -> "); + sb.append(def.uniqueName()); + sb.append(" [style=dotted taillabel=").append(i).append("];\n"); + } else if( def != null ) { + // Most edges land here use->def + sb.append('\t').append(n.uniqueName()).append(" -> "); + if( def instanceof CProjNode proj ) { + String mname = proj.ctrl().uniqueName(); + sb.append(mname).append(":p").append(proj._idx); + } else if( def instanceof ProjNode proj ) { + String mname = proj.in(0).uniqueName(); + sb.append(mname).append(":p").append(proj._idx); + } else sb.append(def.uniqueName()); + // Number edges, so we can see how they track + sb.append("[taillabel=").append(i); + // The edge from New to ctrl is just for anchoring the New + if ( n instanceof NewNode ) + sb.append(" color=green"); + // control edges are colored red + else if( def instanceof CFGNode ) + sb.append(" color=red"); + else if( def.isMem() ) + sb.append(" color=blue"); + // Backedges do not add a ranking constraint + if( i==2 && (n instanceof PhiNode || n instanceof LoopNode) ) + sb.append(" constraint=false"); + sb.append("];\n"); + } + } + } + } + + // Walk the scope edges + private void scopeEdges( StringBuilder sb, ScopeNode scope ) { + sb.append("\tedge [style=dashed color=cornflowerblue];\n"); + int level=0; + for( var v : scope._vars ) { + Node def = scope.in(v._idx); + while( def instanceof ScopeNode lazy ) + def = lazy.in(v._idx); + if( def==null ) continue; + while( level < scope._lexSize.size() && v._idx >= scope._lexSize.at(level) ) + level++; + String scopeName = makeScopeName(scope, level-1); + sb.append("\t") + .append(scopeName).append(":") + .append('"').append(makePortName(scopeName, v._name)).append('"') // wrap port name with quotes because $ctrl is not valid unquoted + .append(" -> "); + if( def instanceof CProjNode proj ) { + sb.append(def.in(0).uniqueName()).append(":p").append(proj._idx); + } else if( def instanceof ProjNode proj ) { + sb.append(def.in(0).uniqueName()).append(":p").append(proj._idx); + } else sb.append(def.uniqueName()); + sb.append(";\n"); + } + } + + /** + * Finds all nodes in the graph. + */ + public static Collection<Node> findAll(Stack<ScopeNode> ss, Node... ns) { + final HashMap<Integer, Node> all = new HashMap<>(); + for( Node n : ns ) + walk(all, n); + if( ss != null ) + for( Node n : ss ) + walk(all, n); + return all.values(); + } + + /** + * Walk a subgraph and populate distinct nodes in the all list. + */ + private static void walk(HashMap<Integer,Node> all, Node n) { + if(n == null ) return; + if (all.get(n._nid) != null) return; // Been there, done that + all.put(n._nid, n); + for (Node c : n._inputs) + walk(all, c); + for( Node c : n._outputs ) + walk(all, c); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/IRPrinter.java b/chapter20/src/main/java/com/seaofnodes/simple/IRPrinter.java new file mode 100644 index 00000000..3cddacfd --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/IRPrinter.java @@ -0,0 +1,279 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; + +public abstract class IRPrinter { + + // Print a node on 1 line, columnar aligned, as: + // NNID NNAME DDEF DDEF [[ UUSE UUSE ]] TYPE + // 1234 sssss 1234 1234 1234 1234 1234 1234 tttttt + public static SB printLine( Node n, SB sb ) { + sb.p("%4d %-7.7s ".formatted(n._nid,n.label())); + if( n._inputs==null ) + return sb.p("DEAD\n"); + for( Node def : n._inputs ) + sb.p(def==null ? "____" : "%4d".formatted(def._nid)) + // Lazy Phi indicator + .p(n instanceof MemMergeNode && def instanceof MemMergeNode ? "^" : " "); + for( int i = n._inputs.size(); i<4; i++ ) + sb.p(" "); + sb.p(" [[ "); + for( Node use : n._outputs ) + sb.p(use==null ? "____ " : "%4d ".formatted(use._nid)); + int lim = 6 - Math.max(n._inputs.size(),4); + for( int i = n._outputs.size(); i<lim; i++ ) + sb.p(" "); + sb.p(" ]] "); + if( n._type!= null ) sb.p(n._type.str()); + return sb.p("\n"); + } + + + public static String prettyPrint(Node node, int depth) { + return CodeGen.CODE._phase.ordinal() > CodeGen.Phase.Schedule.ordinal() + ? _prettyPrintScheduled( node, depth ) + : _prettyPrint( node, depth ); + } + + // Another bulk pretty-printer. Makes more effort at basic-block grouping. + private static String _prettyPrint( Node node, int depth ) { + // First, a Breadth First Search at a fixed depth. + BFS bfs = new BFS(node,depth); + // Convert just that set to a post-order + ArrayList<Node> rpos = new ArrayList<>(); + BitSet visit = new BitSet(); + for( int i=bfs._lim; i< bfs._bfs.size(); i++ ) + postOrd( bfs._bfs.get(i), null, rpos, visit, bfs._bs); + // Reverse the post-order walk + SB sb = new SB(); + boolean gap=false; + for( int i=rpos.size()-1; i>=0; i-- ) { + Node n = rpos.get(i); + if( n instanceof CFGNode || n.isMultiHead() ) { + if( !gap ) sb.p("\n"); // Blank before multihead + if( n instanceof FunNode fun ) + fun.sig().print(sb.p("--- ").p(fun._name==null ? "" : fun._name).p(" "),false).p("----------------------\n"); + printLine( n, sb ); // Print head + while( --i >= 0 ) { + Node t = rpos.get(i); + if( !t.isMultiTail() ) { i++; break; } + printLine( t, sb ); + } + if( n instanceof ReturnNode ret ) { + FunNode fun = ret.fun(); + fun.sig().print(sb.p("--- ").p(fun._name==null ? "" : fun._name).p(" "),false).p("----------------------\n"); + } + if( !(n instanceof CallNode) ) { + sb.p("\n"); // Blank after multitail + gap = true; + } + } else { + printLine( n, sb ); + gap = false; + } + } + return sb.toString(); + } + + private static void postOrd(Node n, Node prior, ArrayList<Node> rpos, BitSet visit, BitSet bfs) { + if( !bfs.get(n._nid) ) + return; // Not in the BFS visit + if( n instanceof FunNode && !(prior instanceof StartNode) ) + return; // Only visit Fun from Start + if( visit.get(n._nid) ) return; // Already post-order walked + visit.set(n._nid); + // First walk the CFG, then everything + if( n instanceof CFGNode ) { + for( Node use : n._outputs ) + // Follow CFG, not across call/function borders, and not around backedges + if( use instanceof CFGNode && !(n instanceof CallNode && use instanceof FunNode) && + use.nOuts() >= 1 && !(use._outputs.get(0) instanceof LoopNode) ) + postOrd(use, n, rpos,visit,bfs); + for( Node use : n._outputs ) + // Follow CFG, not across call/function borders + if( use instanceof CFGNode && !(n instanceof CallNode && use instanceof FunNode) ) + postOrd(use,n,rpos,visit,bfs); + } + // Follow all outputs + for( Node use : n._outputs ) + if( use != null && + !(n instanceof CallNode && use instanceof FunNode) && + (n instanceof FunNode || !(use instanceof ParmNode)) ) + postOrd(use, n, rpos,visit,bfs); + // Post-order + rpos.add(n); + } + + // Breadth-first search, broken out in a class to keep in more independent. + // Maintains a root-set of Nodes at the limit (or past by 1 if MultiHead). + public static class BFS { + // A breadth first search, plus MultiHeads for any MultiTails + public final ArrayList<Node> _bfs; + public final BitSet _bs; // Visited members by node id + public final int _depth; // Depth limit + public final int _lim; // From here to _bfs._len can be roots for a reverse search + public BFS( Node base, int d ) { + _depth = d; + _bfs = new ArrayList<>(); + _bs = new BitSet(); + + add(base); // Prime the pump + int idx=0, lim=1; // Limit is where depth counter changes + while( idx < _bfs.size() ) { // Ran out of nodes below depth + Node n = _bfs.get(idx++); + for( Node def : n._inputs ) + if( def!=null && !_bs.get(def._nid) ) + add(def); + if( idx==lim ) { // Depth counter changes at limit + if( --d < 0 ) + break; // Ran out of depth + lim = _bfs.size(); // New depth limit + } + } + // Toss things past the limit except multi-heads + while( idx < _bfs.size() ) { + Node n = _bfs.get(idx); + if( n.isMultiHead() ) idx++; + else del(idx); + } + // Root set is any node with no inputs in the visited set + lim = _bfs.size(); + for( int i=_bfs.size()-1; i>=0; i-- ) + if( !any_visited(_bfs.get(i)) ) + swap( i,--lim); + _lim = lim; + } + void swap( int x, int y ) { + if( x==y ) return; + Node tx = _bfs.get(x); + Node ty = _bfs.get(y); + _bfs.set(x,ty); + _bfs.set(y,tx); + } + void add(Node n) { + _bfs.add(n); + _bs.set(n._nid); + } + void del(int idx) { + _bs.clear(_bfs.get(idx)._nid); + Utils.del(_bfs, idx); + } + boolean any_visited( Node n ) { + for( Node def : n._inputs ) + if( def!=null && _bs.get(def._nid) ) + return true; + return false; + } + } + + // Bulk pretty printer, knowing scheduling information is available + private static String _prettyPrintScheduled( Node node, int depth ) { + // Backwards DFS walk to depth. + HashMap<Integer,Integer> ds = new HashMap<>(); + Ary<Node> ns = new Ary<>(Node.class); + _walk(ds,ns,node,depth); + // Remove data projections, these are force-printed behind their multinode head + for( int i=0; i<ns.size(); i++ ) { + if( ns.get(i) instanceof ProjNode proj && !(proj.in(0) instanceof CFGNode) ) { + ns.del(i--); + ds.remove(proj._nid); + } + } + // Print by block with least idepth + SB sb = new SB(); + Ary<Node> bns = new Ary<>(Node.class); + while( !ds.isEmpty() ) { + CFGNode blk = null; + for( Node n : ns ) { + CFGNode cfg = n instanceof CFGNode cfg0 && cfg0.blockHead() ? cfg0 : n.cfg0(); + if( blk==null || cfg.idepth() < blk.idepth() ) + blk = cfg; + } + Integer d = ds.remove(blk._nid); + ns.del(ns.find(blk)); + + // Print block header + sb.p("%-13.13s".formatted(label(blk)+":")); + sb.p( " ".repeat(4) ).p(" [[ "); + if( blk instanceof StartNode ) ; + else if( blk instanceof RegionNode || blk instanceof StopNode ) + for( int i=(blk instanceof StopNode ? 3 : 1); i<blk.nIns(); i++ ) + label(sb,blk.cfg(i)); + else + label(sb,blk.cfg(0)); + sb.p(" ]] \n"); + printLine(blk,sb); + + // Collect block contents that are in the depth limit + bns.clear(); + int xd = Integer.MAX_VALUE; + for( Node use : blk._outputs ) { + Integer i = ds.get(use._nid); + if( i!=null && !(use instanceof CFGNode cfg && cfg.blockHead()) ) { + if( bns.find(use)==-1 ) + bns.add(use); + xd = Math.min(xd,i); + } + } + // Print Phis up front, if any + for( int i=0; i<bns.size(); i++ ) + if( bns.get(i) instanceof PhiNode phi ) + printLine( phi, sb,bns,i--,ds,ns); + + // Print block contents in depth order, bumping depth until whole block printed + for( ; !bns.isEmpty(); xd++ ) + for( int i=0; i<bns.size(); i++ ) { + Node n = bns.get(i); + if( ds.get(n._nid)==xd ) { + printLine( n, sb, bns, i--, ds,ns ); + if( n instanceof MultiNode && !(n instanceof CFGNode) ) { + for( Node use : n._outputs ) { + printLine(use,sb,bns,bns.indexOf(use),ds,ns); + } + } + } + } + sb.p("\n"); + } + return sb.toString(); + } + + private static void _walk( HashMap<Integer,Integer> ds, Ary<Node> ns, Node node, int d ) { + Integer nd = ds.get(node._nid); + if( nd!=null && d <= nd ) return; // Been there, done that + Integer old = ds.put(node._nid,d) ; + if( old == null ) + ns.add(node); + if( d == 0 ) return; // Depth cutoff + for( Node def : node._inputs ) + if( def != null && + !(node instanceof LoopNode loop && loop.back()==def) && + // Don't walk into or out of functions + !(node instanceof CallEndNode && def instanceof ReturnNode) && + !(node instanceof FunNode && def instanceof CallNode) && + !(node instanceof ParmNode && !(def instanceof FunNode)) + ) + _walk(ds,ns,def,d-1); + } + + static String label( CFGNode blk ) { + if( blk instanceof StartNode ) return "START"; + return (blk instanceof LoopNode ? "LOOP" : "L")+blk._nid; + } + static void label( SB sb, CFGNode blk ) { + if( !blk.blockHead() ) blk = blk.cfg(0); + sb.p( "%-9.9s ".formatted( label( blk ) ) ); + } + static void printLine( Node n, SB sb, Ary<Node> bns, int i, HashMap<Integer,Integer> ds, Ary<Node> ns ) { + printLine( n, sb ); + if( i != -1 ) bns.del(i); + ds.remove(n._nid); + int idx = ns.find(n); + if( idx!=-1 ) ns.del(idx); + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/IterPeeps.java b/chapter20/src/main/java/com/seaofnodes/simple/IterPeeps.java new file mode 100644 index 00000000..265e0b21 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/IterPeeps.java @@ -0,0 +1,196 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.Random; + +/** + * The IterPeeps runs after parsing. It iterates the peepholes to a fixed point + * so that no more peepholes apply. This should be linear because peepholes rarely + * (never?) increase code size. The graph should monotonically reduce in some + * dimension, which is usually size. It might also reduce in e.g. number of + * MulNodes or Load/Store nodes, swapping out more "expensive" Nodes for cheaper + * ones. + * <br> + * The theoretical overall worklist is mindless just grabbing the next thing and + * doing it. If the graph changes, put the neighbors on the worklist. + * <br> + * Lather, Rinse, Repeat until the worklist runs dry. + * <p> + * The main issues we have to deal with: + * + * <ul> + * <li>Nodes have uses; replacing some set of Nodes with another requires more graph + * reworking. Not rocket science, but it can be fiddly. Its helpful to have a + * small set of graph munging utilities, and the strong invariant that the graph + * is stable and correct between peepholes. In our case `Node.subsume` does + * most of the munging, building on our prior stable Node utilities.</li> + * + * <li>Changing a Node also changes the graph "neighborhood". The neighbors need to + * be checked to see if THEY can also peephole, and so on. After any peephole + * or graph update we put a Nodes uses and defs on the worklist.</li> + * + * <li>Our strong invariant is that for all Nodes, either they are on the worklist + * OR no peephole applies. This invariant is easy to check, although expensive. + * Basically the normal "iterate peepholes to a fixed point" is linear, and this + * check is linear at each peephole step... so quadratic overall. Its a useful + * assert, but one we can disable once the overall algorithm is stable - and + * then turn it back on again when some new set of peepholes is misbehaving. + * The code for this is turned on in `IterPeeps.iterate` as `assert + * progressOnList(stop);`</li> + * </ul> + */ +public class IterPeeps { + + private final WorkList<Node> _work; + + IterPeeps( long seed ) { _work = new WorkList<>(seed); } + + public <N extends Node> N add( N n ) { return (N)_work.push(n); } + + public void addAll( Ary<Node> ary ) { _work.addAll(ary); } + + /** + * Iterate peepholes to a fixed point + */ + public void iterate( CodeGen code ) { + assert progressOnList(code); + int cnt=0; + + Node n; + while( (n=_work.pop()) != null ) { + if( n.isDead() ) continue; + cnt++; // Useful for debugging, searching which peephole broke things + Node x = n.peepholeOpt(); + if( x != null ) { + if( x.isDead() ) continue; + // peepholeOpt can return brand-new nodes, needing an initial type set + if( x._type==null ) x.setType(x.compute()); + // Changes require neighbors onto the worklist + if( x != n || !(x instanceof ConstantNode) ) { + // All outputs of n (changing node) not x (prior existing node). + for( Node z : n._outputs ) _work.push(z); + // Everybody gets a free "go again" in case they didn't get + // made in their final form. + _work.push(x); + // If the result is not self, revisit all inputs (because + // there's a new user), and replace in the graph. + if( x != n ) { + for( Node z : n. _inputs ) _work.push(z); + n.subsume(x); + } + } + // If there are distant neighbors, move to worklist + n.moveDepsToWorklist(); + JSViewer.show(); // Show again + assert progressOnList(code); // Very expensive assert + } + if( n.isUnused() && !(n instanceof StopNode) ) + n.kill(); // Just plain dead + } + + } + + // Visit ALL nodes and confirm the invariant: + // Either you are on the _work worklist OR running `iter()` makes no progress. + + // This invariant ensures that no progress is missed, i.e., when the + // worklist is empty we have indeed done all that can be done. To help + // with debugging, the {@code assert} is broken out in a place where it is easy to + // stop if a change is found. + + // Also, the normal usage of `iter()` may attempt peepholes with distance + // neighbors and these should fail, but will then try to add dependencies + // {@link #Node.addDep} which is a side effect in an assert. The {@link + // #midAssert} is used to stop this side effect. + private boolean progressOnList(CodeGen code) { + code._midAssert = true; + Node changed = code._stop.walk( n -> { + Node m = n; + if( n.compute().isa(n._type) && (!n.iskeep() || n._nid<=6) ) { // Types must be forwards, even if on worklist + if( _work.on(n) ) return null; + m = n.peepholeOpt(); + if( m==null ) return null; + } + System.err.println("BREAK HERE FOR BUG"); + return m; + }); + code._midAssert = false; + return changed==null; + } + + /** + * Classic WorkList, with a fast add/remove, dup removal, random pull. + * The Node's nid is used to check membership in the worklist. + */ + public static class WorkList<E extends Node> { + + private Node[] _es; + private int _len; + private final BitSet _on; // Bit set if Node._nid is on WorkList + private final Random _R; // For randomizing pull from the WorkList + private final long _seed; + + /* Useful stat - how many nodes are processed in the post parse iterative opt */ + private long _totalWork = 0; + + WorkList() { this(123); } + WorkList(long seed) { + _es = new Node[1]; + _len=0; + _on = new BitSet(); + _seed = seed; + _R = new Random(); + _R.setSeed(_seed); + } + + /** + * Pushes a Node on the WorkList, ensuring no duplicates + * If Node is null it will not be added. + */ + public E push( E x ) { + if( x==null ) return null; + int idx = x._nid; + if( !_on.get(idx) ) { + _on.set(idx); + if( _len==_es.length ) + _es = Arrays.copyOf(_es,_len<<1); + _es[_len++] = x; + _totalWork++; + } + return x; + } + + public void addAll( Ary<E> ary ) { + for( E n : ary ) + push(n); + } + + /** + * True if Node is on the WorkList + */ + boolean on( E x ) { return _on.get(x._nid); } + boolean isEmpty() { return _len==0; } + + /** + * Removes a random Node from the WorkList; null if WorkList is empty + */ + E pop() { + if( _len == 0 ) return null; + int idx = _R.nextInt(_len); + E x = (E)_es[idx]; + _es[idx] = _es[--_len]; // Compress array + _on.clear(x._nid); + return x; + } + + public void clear() { + _len = 0; + _on.clear(); + _R.setSeed(_seed); + _totalWork = 0; + } + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/JSViewer.java b/chapter20/src/main/java/com/seaofnodes/simple/JSViewer.java new file mode 100644 index 00000000..fcb0c28a --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/JSViewer.java @@ -0,0 +1,402 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import java.io.IOException; +import java.util.Stack; +import java.nio.file.Paths; +import java.util.Collection; +import static com.seaofnodes.simple.CodeGen.CODE; + +public class JSViewer implements AutoCloseable { + // Display programs in an endless loop + public static void main( String[] args ) throws Exception { + try(var js = new JSViewer()) { + js.run(); + } + } + + static boolean SHOW; + + // WebSocket to the browser for display + static SimpleWebSocket SERVER; + + static int N; // Dot frames + + JSViewer() throws Exception { + // Launch server; handshake + SERVER = new SimpleWebSocket(Paths.get("docs/index.html").toUri(),12345) ; + } + + void run( ) throws Exception { + SERVER.put("!"); + while( true ) { + String src = SERVER.get(); + switch( src ) { + case null: return; + case "null": return; + case "+": break; // Client requests more frames, but we send them all anyway + default: + System.out.println(src); + try { + N=0; + // Use parser scope, xscope when building views + CodeGen code = new CodeGen(src); + SHOW = true; + show(); + // Parse program, generating views at every parse point + code.parse(); + // No longer user parse internal state when building views + code.opto(); + + // Catch and ignore Parser errors + } catch(RuntimeException re) { + System.err.println(re); + } finally { + SHOW = false; + SERVER.put("#"); // Final frame + } + break; + } + } + } + + @Override public void close() throws IOException { + SERVER.close(); + SERVER=null; + } + + public static void show() { if( SERVER!=null && SHOW ) _show(); } + private static void _show() { + // Skip util we at least get the Parse object made + if( CODE._phase==null || CODE._phase.ordinal() < CodeGen.Phase.Parse.ordinal() ) + return; + boolean midParse = CODE._phase == CodeGen.Phase.Parse; + Stack<ScopeNode> xScopes = midParse ? CODE.P._xScopes : null; + + Collection<Node> all = GraphVisualizer.findAll(xScopes, CODE._stop, midParse ? CODE.P._scope: null); + SB sb = new SB(); + sb.p("digraph view_").p(N++).p(" {\n").ii(); + if( midParse ) + sb.i().p("// POS: ").p(CODE.P.pos()).nl(); + + // To keep the Scopes below the graph and pointing up into the graph we + // need to group the Nodes in a subgraph cluster, and the scopes into a + // different subgraph cluster. THEN we can draw edges between the + // scopes and nodes. If we try to cross subgraph cluster borders while + // still making the subgraphs DOT gets confused. + sb.i().p("rankdir=BT;\n"); // Force Nodes before Scopes + + // CNC - turned off Apr/8/2024, gives more flex in the layout and + // removes some of the more ludicrous layout choices. + // Preserve node input order + //sb.append("ordering=\"in\";\n"); + + // Merge multiple edges hitting the same node. Makes common shared + // nodes much prettier to look at. + sb.i().p("concentrate=\"true\";\n"); + + // Force nested scopes to order + sb.i().p("compound=\"true\";\n"); + + // Just the Nodes first, in a cluster no edges + nodes(sb, all); + + // Now the scopes, in a cluster no edges + if( midParse ) + for( ScopeNode xscope : xScopes ) + if( !xscope.isDead() ) + scope( sb, xscope ); + + // Walk the Node edges + nodeEdges(sb, midParse && !xScopes.empty() ? xScopes.peek() : null, all); + + // Walk the active Scope edges + if( midParse ) + for( ScopeNode xscope : xScopes ) + if( !xscope.isDead() ) + scopeEdges( sb, xscope ); + + + sb.p("}\n").di(); + // Tell client another DOT frame + String dot = sb.toString(); + System.out.println(dot); + try { + SERVER.put(dot); + } catch( IOException ioe ) { + try { SERVER.close();} catch( IOException ignored ){} + } + } + + + private static void nodes(SB sb, Collection<Node> all) { + // Just the Nodes first, in a cluster no edges + sb.i().p("subgraph cluster_Nodes {\n").ii(); // Magic "cluster_" in the subgraph name + for( Node n : all ) { + if( n instanceof ProjNode || n instanceof CProjNode || n instanceof MemMergeNode ) + continue; // Do not emit, rolled into MultiNode or Scope cluster already + sb.i().p(n.uniqueName()).p(" [ "); + if( n instanceof MultiNode && !(n instanceof StartNode) ) { + // Make a box with the MultiNode on top, and all the projections on the bottom + sb. p("shape=plaintext label=<\n").ii(); + sb.i().p("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"4\">\n"); + // Row over single cell for node label + cell(sb.i().p("<TR>"), n.glabel(), n, null).p("</TR>\n"); + // Row over single cell, for nested table + sb.i().p("<TR><TD>\n").ii(); + sb.i().p("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">").p("\n"); + sb.i().p("<TR>"); + n._outputs.sort((x,y) -> x instanceof ProjNode xp && y instanceof ProjNode yp ? (xp._idx - yp._idx) : ((x==null ? 99999 : x._nid) - (y==null ? 99999 : y._nid))); + boolean empty_row=true; + for( Node use : n._outputs ) { + int idx = idx(n); + if( idx != -1 ) { + cell(sb,use.glabel(),use,"p"+idx); + empty_row=false; + } + } + // At least one cell on row + if( empty_row ) sb.p("<TD></TD>"); + sb. p("</TR>").p("\n"); + sb.i().p("</TABLE>").p("\n").di(); + sb.i().p("</TD></TR>\n"); + sb.i().p("</TABLE>>\n").di(); + sb.i().p("];\n"); + + } else { + // control nodes have box shape + // other nodes are ellipses, i.e. default shape + //else if (n instanceof PhiNode) sb.p("style=filled fillcolor=lightyellow "); + node(sb,n.glabel(),n); + sb.p("];\n"); + } + } + // Force Region & Phis to line up + for (Node n : all) { + if (n instanceof RegionNode region) { + sb.i().p("{ rank=same; "); + sb.p(region.uniqueName()).p(";"); + for (Node phi : region._outputs) + if (phi instanceof PhiNode) sb.p(phi.uniqueName()).p(";"); + sb.p("}\n"); + } + } + sb.di().i().p("}").nl(); // End Node cluster + } + + // Build a nested scope display, walking the _prev edge + private static void scope( SB sb, ScopeNode scope ) { + sb.i().p("node [shape=plaintext];\n"); + int last = scope.nIns(); + int max = scope._lexSize.size()+1; // One more than lexsize + for( int i = 0; i < max; i++ ) { + int level = max-i-1; + String scopeName = makeScopeName(scope, level); + sb.i().p("subgraph cluster_").p(scopeName).p(" {\n").ii(); // Magic "cluster_" in the subgraph name + sb.i().p(scopeName).p(" [label=<\n").ii(); + sb.i().p("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">\n"); + int lexStart = level==0 ? 0 : scope._lexSize.at(level-1); + + // Special for memory ScopeMinNode + MemMergeNode n = scope.nIns()>1 ? scope.mem() : null; + if( level==0 && n!=null && n.nIns()>2 ) { + sb.i().p("<TR>"); + for( int m=2; m<n.nIns(); m++ ) + cell(sb,"#"+m,n.alias(m),"m"+m); + sb.p("</TR>\n"); // End scope level + } + + // Scope variables, empty if none + if( lexStart<last ) { + sb.i().p("<TR>\n"); + for( int j=lexStart; j<last; j++ ) { + if( scope.in(j)==null ) continue; + var v = scope._vars.at(j); + cell(sb.i(),v._name,scope.in(j),makePortName(scopeName, v._name)).nl(); + } + last = lexStart; + sb.i().p("</TR>\n"); + } + sb.i().p("</TABLE>>];\n").di(); + // Scope label + sb.i().p(scopeName).p("A [label=\"").p(level).p("\" shape=doublecircle style=filled fillcolor=aqua fontsize=10 margin=0 width=0.2 ];\n"); + } + // Scope clusters nest, so the graphics shows the nested scopes, so + // they are not closed as they are printed; so they just keep nesting. + // We close them all at once here. + for( int i=0; i<max; i++ ) + sb.di().i().p("}\n"); + } + + // Append a cell, with color + private static SB cell(SB sb, String text, Node n, String port) { + sb.p("<TD"); + String color = color(n); + sb.p(" BGCOLOR=\"").p(color).p("\""); + if( port!=null ) sb.p(" PORT=\"").p(port).p("\""); + return colorcell(sb.p(">"),text,n,dark(color)).p("</TD>"); + } + private static SB node(SB sb, String text, Node n) { + String color = color(n); + sb.p("style=filled fillcolor=").p(color).p(" "); + String shape = switch( n ) { + case FunNode fun -> "shape=box peripheries=2"; + case CFGNode cfg -> "shape=box"; + default -> ""; + }; + sb.p(shape).p(" "); + return colorcell(sb.p("label=<"),text,n,dark(color)).p(">"); + } + + private static String color(Node n) { + return switch( n ) { + case CFGNode cfg -> "yellow"; + case NewNode nnn -> "lightgreen"; + case StructNode s -> "lightgreen"; + default -> typeColor(n._type); + }; + } + private static String typeColor(Type t) { + return t==null ? "white" : switch( t ) { + case TypeMem mem -> "blue"; + case TypeMemPtr tmp -> "green"; + case TypeFunPtr tfp -> "orange"; + case TypeRPC rpc -> "antiquewhite"; + case TypeInteger ti -> "lightblue"; + case TypeFloat tf -> "aqua"; + case Type tn -> tn==Type.NIL ? "lightgray" : "red"; + }; + } + private static final String[] DARKS = new String[]{"blue","green","black"}; + private static boolean dark(String color) { return Utils.find(DARKS,color) != -1; } + + + + // Called with an open div + private static SB colorcell(SB sb, String text, Node n, boolean dark) { + if( dark ) sb.p("<font color=\"white\">"); + sb.p(text); + if( n._type!=null ) { + sb.p("<br /><font point-size=\"10\">"); + n._type.gprint(sb); + sb.p("</font>"); + } + if( dark ) sb.p("</font>"); + return sb; + } + + + private static String makeScopeName(ScopeNode sn, int level) { return sn.uniqueName() + "_" + level; } + private static String makePortName(String scopeName, String varName) { return scopeName + "_" + varName; } + + // Walk the node edges + private static void nodeEdges(SB sb, ScopeNode scope, Collection<Node> all) { + // All them edge labels + String scopeName = scope!=null ? makeScopeName(scope, 0) : null; + sb.i().p("edge [ fontname=Helvetica, fontsize=8 ];\n"); + for( Node n : all ) { + // Do not display the Constant->Start edge; + if( n instanceof ConstantNode || + n instanceof XCtrlNode || + // ProjNodes handled by Multi; + n instanceof ProjNode || + n instanceof CProjNode || + // ScopeNodes are done separately + n instanceof ScopeNode || + n instanceof MemMergeNode + ) + continue; + if( n.isDead() ) + throw Utils.TODO(); + // Normal edges + for( int i=0; i<n.nIns(); i++ ) { + Node def = n.in(i); + if( def==null ) continue; + sb.i().p(n.uniqueName()).p(" -> "); + if( n instanceof PhiNode && def instanceof RegionNode ) { + // Draw a dotted use->def edge from Phi to Region. + sb.p(def.uniqueName()); + sb.p(" [style=dotted taillabel=").p(i).p("];\n"); + } else { + // Most edges land here use->def + if( def instanceof CProjNode proj ) { + String mname = proj.ctrl().uniqueName(); + sb.p(mname).p(":p").p(proj._idx); + } else if( def instanceof ProjNode proj ) { + String mname = proj.in(0).uniqueName(); + sb.p(mname).p(":p").p(proj._idx); + } else sb.p(def.uniqueName()); + // Number edges, so we can see how they track + sb.p("[taillabel=").p(i); + // Color the edge + nodeEdgeColor(sb,def._type); + // Backedges do not add a ranking constraint + if( i==2 && (n instanceof PhiNode || n instanceof LoopNode) ) + sb.p(" constraint=false"); + sb.p("];\n"); + } + } + + // Bonus edge if hooked by parser + if( (n.iskeep() || n.isUnused()) && scopeName != null ) { + sb.i().p(scopeName).p(" -> ").p(n.uniqueName()).p(" [ style=dashed color=grey];\n"); + } + // Force functions above return + if( n instanceof ReturnNode ret && ret.inProgress() && !ret.fun().isDead() ) + sb.i().p(ret.ctrl().uniqueName()).p(" -> ").p(ret.fun().uniqueName()).p("[style=invis]\n"); + } + } + + private static void nodeEdgeColor(SB sb, Type t) { + sb.p(" color=").p(typeColor(t)); + } + + // Walk the scope edges + private static void scopeEdges( SB sb, ScopeNode scope ) { + sb.i().p("edge [style=dashed color=cornflowerblue];\n"); + int level=0; + for( var v : scope._vars ) { + if( v._idx==1 ) continue; // Memory done separately + Node def = scope.in(v._idx); + while( def instanceof ScopeNode lazy ) + def = lazy.in(v._idx); + if( def==null ) continue; + while( level < scope._lexSize.size() && v._idx >= scope._lexSize.at(level) ) + level++; + String scopeName = makeScopeName(scope, level); + sb.i() + .p(scopeName).p(":") + .p('"').p(makePortName(scopeName, v._name)).p('"') // wrap port name with quotes because $ctrl is not valid unquoted + .p(" -> "); + defPort(sb,def).p(";\n"); + } + + // Memory + if( scope.nIns()>1 ) { + MemMergeNode n = scope.mem(); + for( int i=2; i<n.nIns(); i++ ) { + Node def = n.in(i); + while( def instanceof ScopeNode lazy ) + def = lazy.in(i); + if( def==null ) continue; + String scopeName = makeScopeName(scope, 0); + sb.i().p(scopeName).p(":m").p(i).p(" -> "); + defPort(sb,def).p(";\n"); + } + } + + } + + // (C)Projection index or -1 + private static int idx(Node n) { + return n instanceof ProjNode p ? p._idx : (n instanceof CProjNode cp ? cp._idx : -1); + } + + private static SB defPort(SB sb, Node def) { + int idx = idx(def); + return idx != -1 + ? sb.p(def.in(0).uniqueName()).p(":p").p(idx) + : sb.p(def.uniqueName()); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/ListScheduler.java b/chapter20/src/main/java/com/seaofnodes/simple/ListScheduler.java new file mode 100644 index 00000000..18055e19 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/ListScheduler.java @@ -0,0 +1,231 @@ + package com.seaofnodes.simple; + +import com.seaofnodes.simple.IterPeeps.WorkList; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import java.util.*; + +public abstract class ListScheduler { + + // eXtra per-node stuff for scheduling. + private static final IdentityHashMap<Node,XSched> XS = new IdentityHashMap<>(); + private static class XSched { + static final Ary<XSched> FREE = new Ary<>(XSched.class); + static void alloc(CFGNode bb, Node n) { + XSched x = FREE.pop(); + XS.put(n, (x==null ? new XSched() : x).init(bb,n)); + } + void free() { _n = null; FREE.push(this); } + + static XSched get(Node n) { return n==null ? null : XS.get(n); } + + private XSched init(CFGNode bb, Node n) { + _n = n; + _bcnt = 0; + _ruse = _rdef = _single = false; + + // Count block-locals and remote inputs + if( !(n instanceof PhiNode) ) + for( Node def : n._inputs ) + if( def != null && def != bb ) { + if( def.cfg0()==bb ) _bcnt++; // Raise block-local input count + else _ruse = true; + } + // Count remote outputs and allowed registers + if( n instanceof MultiNode ) + for( Node use : n._inputs ) + computeSingleRDef(bb,use); + else + computeSingleRDef(bb,n); + return this; + } + + private void computeSingleRDef(CFGNode bb, Node n) { + RegMaskRW rmask = n instanceof MachNode mach ? mach.outregmap().copy() : null; + for( Node use : n._outputs ) { + // Remote use, so this is a remote def + if( use!=null && use.cfg0()!=bb ) _rdef = true; + // And mask between n's def mask and all n's users masks' + if( use instanceof MachNode mach && rmask != null ) + for( int i=1; i<use.nIns(); i++ ) + if( use.in(i)==n ) + rmask.and(mach.regmap(i)); + } + // Defines in a single register + _single = rmask!=null && rmask.size1(); + } + + // If _bcnt==0, declare ready; move user bcnts into rcnts. + boolean isReady() { + if( _bcnt > 0 || _rcnt > 0 ) return false; + if( _n instanceof MultiNode ) + for( Node use : _n._outputs ) + if( use instanceof ProjNode ) readyUp(use); + // anti-dep from a multireg directly + else _readyUp(use); + else + readyUp(_n); + return true; + } + private static void readyUp(Node n) { + for( Node use : n._outputs ) + _readyUp(use); + } + private static void _readyUp(Node use) { + XSched xs = get(use); + if( xs!=null && !(use instanceof PhiNode) ) + { assert xs._bcnt>0; xs._bcnt--; xs._rcnt++; } + } + + boolean decIsReady() { + assert _rcnt > 0; + _rcnt--; + return isReady(); + } + + Node _n; // Node this extra is for + int _bcnt; // Not-ready not-scheduled inputs + int _rcnt; // Ready not-scheduled inputs + boolean _ruse; // Node IS a "remote use", uses a other-block value + boolean _rdef; // Node IS a "remote def" of a value used in other block + boolean _single; // Defines a single register, likely to conflict + } + + + // List schedule every block + public static void sched( CodeGen code ) { + XS.clear(); + for( CFGNode bb : code._cfg ) + if( bb.blockHead() ) + local(bb); + } + + // Classic list scheduler + private static void local( CFGNode bb ) { + assert XS.isEmpty(); + int len = bb.nOuts(); + + // Count block-locals, remote inputs and outputs + for( int i=0; i<len; i++ ) + XSched.alloc(bb,bb.out(i)); + + // Nodes are ready if they are not used by other nodes in this block. + // Move ready up front. + int ready = 0; + for( int i=0; i<len; i++ ) + if( XSched.get(bb.out(i)).isReady() ) // No block-local inputs? + bb._outputs.swap(ready++,i); // Move into ready set + assert len==0 || ready > 0; + + // Classic list scheduler. Behind sched is scheduled. Between sched + // and ready have zero counts and are ready to schedule. Ahead of + // ready is undefined. + // + // As nodes are scheduled, the nodes they use decrement their bcnts[]. Once + // that gets to 0, all the nodes using it are scheduled, so it's ready. + int sched = 0; + while( sched < len ) { + assert sched < ready; + int pick = best(bb._outputs,sched,ready); // Pick best + Node best = bb._outputs.swap(pick,sched++); // Swap best to head of ready and move into the scheduled set + // Lower ready count of users, and since projections are ready as + // soon as their multi is ready, lower those now too. + for( Node use : best._outputs ) + if( use instanceof ProjNode ) + for( Node useuse : use._outputs ) + ready = ready(bb,useuse,ready); + else + ready = ready(bb,use,ready); + } + + // Reset for next block + for( XSched x : XS.values() ) + x.free(); + XS.clear(); + } + + private static int ready(CFGNode bb, Node use, int ready) { + if( use!=null && !(use instanceof PhiNode) && use.in(0)==bb && XSched.get(use).decIsReady() ) + bb._outputs.set(ready++, use); // Became ready, move into ready set + //bb._outputs.swap(ready++,use); // Became ready, move into ready set + return ready; + } + + + // Pick best between sched and ready. + private static int best( Ary<Node> blk, int sched, int ready ) { + int pick = sched; + int score = score(blk.at(pick)); + for( int i=sched+1; i<ready; i++ ) { + int nscore = score(blk.at(i)); + if( nscore > score ) + { score = nscore; pick = i; } + } + return pick; + } + + // Highest score wins. Max at 1000, min at 10, except specials. + private static final int[] CNT = new int[3]; + static int score( Node n ) { + if( n instanceof ProjNode ) return 1001; // Pinned at block entry + if( n instanceof CProjNode ) return 1001; // Pinned at block entry + if( n instanceof PhiNode ) return 1000; + if( n instanceof CFGNode ) return 1; // Pinned at block exit + + int score = 500; + // Ideal nodes ignore register pressure scheduling + if( !(n instanceof MachNode) ) + return score; + + // Register pressure local scheduling: avoid overlapping specialized + // registers usages. + + // Subtract 10 if this op forces a live range to exist that cannot be + // resolved immediately; i.e., live count goes up. Scale to 20 if + // multi-def. + + // Subtract 100 if this op forces a single-def live range to exist + // which might conflict on the same register with other live ranges. + // Defines a single register based on def & uses, and the output is not + // ready. Scale to 200 for multi-def. + CNT[1]=CNT[2]=0; + XSched xn = XSched.get(n); + if( xn._rdef ) score = 200; // If defining a remote value, just generically stall alot + if( n instanceof MultiNode ) { + for( Node use : n._outputs ) + singleUseNotReady( use, xn._single ); + } else if( n.nOuts() == 1 ) + singleUseNotReady( n, xn._single ); + score += -10 * Math.min( CNT[1], 2 ); + score += -100 * Math.min( CNT[2], 2 ); + + // Add 10 if this op ends a single-def live range, add 20 for 2 or more. + // Scale to 100,200 if the single-def is also single-register. + CNT[1]=CNT[2]=0; + for( int i=1; i<n.nIns(); i++ ) { + XSched xd = XSched.get( n.in(i) ); + if( xd != null && xd._n.nOuts()==1 ) + CNT[xd._single || ((MachNode)n).regmap(i).size1() ? 2 : 1]++; + } + score += 10 * Math.min( CNT[1], 2 ); + score += 100 * Math.min( CNT[2], 2 ); + + //// Nothing special + //if( n instanceof Pe ) return 500; // RISC want to go after PE + //if( n instanceof Risc ) return 400; // RISC want to go after PE + assert 10 <= score && score <= 990; + return score; + } + + // Single use, and that use is not ready to fire once this one does. + // If true, stalling 'n' might reduce 'n's lifetime. + // Return 0 for false, 1 if true, 10 if also single register + private static void singleUseNotReady( Node n, boolean single ) { + if( n.nOuts() != 1 ) return; + XSched xu = XSched.get(n.out(0)); + if( xu !=null && xu._bcnt==0 ) + return; + CNT[single ? 2 : 1]++; + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/Machine.java b/chapter20/src/main/java/com/seaofnodes/simple/Machine.java new file mode 100644 index 00000000..b29e9aba --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/Machine.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.node.CFGNode; +import com.seaofnodes.simple.node.IfNode; +import com.seaofnodes.simple.node.Node; + +abstract public class Machine { + // Human readable machine name. Something like "x86-64" or "arm" or "risc5" + public abstract String name(); + // Human-readable name for a register number, e.g. "RAX" or "R0" + public abstract String reg( int reg ); + // Create a split op; any register to any register, including stack slots + public abstract Node split(); + // Return a MachNode unconditional branch + public abstract CFGNode jump(); + // Break an infinite loop + public abstract IfNode never( CFGNode ctrl ); + // Instruction select from ideal nodes + public abstract Node instSelect( Node n ); +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/Parser.java b/chapter20/src/main/java/com/seaofnodes/simple/Parser.java new file mode 100644 index 00000000..61a53d68 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/Parser.java @@ -0,0 +1,1729 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import java.text.ParseException; +import java.util.*; + +/** + * The Parser converts a Simple source program to the Sea of Nodes intermediate + * representation directly in one pass. There is no intermediate Abstract + * Syntax Tree structure. + * <p> + * This is a simple recursive descent parser. All lexical analysis is done here as well. + */ +public class Parser { + + public static ConstantNode ZERO; // Very common node, cached here + public static ConstantNode NIL; // Very common node, cached here + public static XCtrlNode XCTRL; // Very common node, cached here + + // Compile driver + public final CodeGen _code; + + // The Lexer. Thin wrapper over a byte[] buffer with a cursor. + private final Lexer _lexer; + + /** + * Current ScopeNode - ScopeNodes change as we parse code, but at any point of time + * there is one current ScopeNode. The reason the current ScopeNode can change is to do with how + * we handle branching. See {@link #parseIf()}. + * <p> + * Each ScopeNode contains a stack of lexical scopes, each scope is a symbol table that binds + * variable names to Nodes. The top of this stack represents current scope. + * <p> + * We keep a list of all ScopeNodes so that we can show them in graphs. + * @see #parseIf() + * @see #_xScopes + */ + public ScopeNode _scope; + + /** + * List of keywords disallowed as identifiers + */ + public static final HashSet<String> KEYWORDS = new HashSet<>(){{ + add("bool"); + add("break"); + add("byte"); + add("continue"); + add("else"); + add("f32"); + add("f64"); + add("false"); + add("flt"); + add("i16"); + add("i32"); + add("i64"); + add("i8"); + add("if"); + add("int"); + add("new"); + add("null"); + add("return"); + add("struct"); + add("true"); + add("u1"); + add("u16"); + add("u32"); + add("u8"); + add("while"); + }}; + + /** + * We clone ScopeNodes when control flows branch; it is useful to have + * a list of all active ScopeNodes for purposes of visualization of the SoN graph + */ + public final Stack<ScopeNode> _xScopes = new Stack<>(); + + ScopeNode _continueScope; + ScopeNode _breakScope; // Merge all the while-breaks here + FunNode _fun; // Current function being parsed + + // Mapping from a type name to a Type. The string name matches + // `type.str()` call. No TypeMemPtrs are in here, because Simple does not + // have C-style '*ptr' references. + public static HashMap<String, Type> TYPES; + + // Mapping from a type name to the constructor for a Type. + public final HashMap<String, StructNode> INITS; + + + public Parser(CodeGen code, TypeInteger arg) { + _code = code; + _lexer = new Lexer(code._src); + _scope = new ScopeNode(); + _continueScope = _breakScope = null; + ZERO = con(TypeInteger.ZERO).keep(); + NIL = con(Type.NIL).keep(); + XCTRL= new XCtrlNode().peephole().keep(); + TYPES = defaultTypes(); + INITS = new HashMap<>(); + } + + @Override + public String toString() { return _lexer.toString(); } + + public static HashMap<String, Type> defaultTypes() { + return new HashMap<>() {{ + put("bool",TypeInteger.U1 ); + put("byte",TypeInteger.U8 ); + put("f32" ,TypeFloat .F32); + put("f64" ,TypeFloat .F64); + put("flt" ,TypeFloat .F64); + put("i16" ,TypeInteger.I16); + put("i32" ,TypeInteger.I32); + put("i64" ,TypeInteger.BOT); + put("i8" ,TypeInteger.I8 ); + put("int" ,TypeInteger.BOT); + put("u1" ,TypeInteger.U1 ); + 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 + }}; + } + + // Debugging utility to find a Node by index + public Node f(int nid) { return _code.f(nid); } + + private Node ctrl() { return _scope.ctrl(); } + private <N extends Node> N ctrl(N n) { return _scope.ctrl(n); } + + public void parse() { parse(false); } + public void parse(boolean show) { + _xScopes.push(_scope); + _scope.define(ScopeNode.CTRL, Type.CONTROL , false, null, _lexer); + _scope.define(ScopeNode.MEM0, TypeMem.BOT , false, null, _lexer); + _scope.define(ScopeNode.ARG0, TypeInteger.BOT, false, null, _lexer); + + ctrl(XCTRL); + _scope.mem(new MemMergeNode(false)); + + // Parse whole program, as-if function header "{ int arg -> body }" + parseFunctionBody(_code._main,loc(),"arg"); + _code.link(_code._main)._name = "main"; + + if( !_lexer.isEOF() ) throw _errorSyntax("unexpected"); + + // Clean up and reset + _xScopes.pop(); + _scope.kill(); + for( StructNode init : INITS.values() ) + init.unkeep().kill(); + INITS.clear(); + _code._stop.peephole(); + if( show ) showGraph(); + } + + /** + * Parses a function body, assuming the header is parsed. + */ + private ReturnNode parseFunctionBody(TypeFunPtr sig, Lexer loc, String... ids) { + // Stack parser state on the local Java stack, and unstack it later + Node oldctrl = ctrl().keep(); + Node oldmem = _scope.mem().keep(); + FunNode oldfun = _fun; + ScopeNode breakScope = _breakScope; _breakScope = null; + ScopeNode continueScope = _continueScope; _continueScope = null; + + FunNode fun = _fun = (FunNode)peep(new FunNode(loc(),sig,null,_code._start)); + // Once the function header is available, install in linker table - + // allowing recursive functions. Linker matches on declared args and + // exact fidx, and ignores the return (because the fidx will only match + // the exact single function). + _code.link(fun); + + Node rpc = new ParmNode("$rpc",0,TypeRPC.BOT,fun,con(TypeRPC.BOT)).peephole(); + + // Build a multi-exit return point for all function returns + RegionNode r = new RegionNode((Lexer)null,null,null).init(); + assert r.inProgress(); + PhiNode rmem = new PhiNode(ScopeNode.MEM0,TypeMem.BOT,r,null).init(); + PhiNode rrez = new PhiNode(ScopeNode.ARG0,Type.BOTTOM,r,null).init(); + ReturnNode ret = new ReturnNode(r, rmem, rrez, rpc, fun).init(); + fun.setRet(ret); + assert ret.inProgress(); + _code._stop.addDef(ret); + + // Pre-call the function from Start, with worse-case arguments. This + // represents all the future, yet-to-be-parsed functions calls and + // external calls. + _scope.push(ScopeNode.Kind.Function); + ctrl(fun); // Scope control from function + // Private mem alias tracking per function + MemMergeNode mem = new MemMergeNode(true); + mem.addDef(null); // Alias#0 + mem.addDef(new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,con(TypeMem.BOT)).peephole()); // All aliases + _scope.mem(mem); + // All args, "as-if" called externally + for( int i=0; i<ids.length; i++ ) { + Type t = sig.arg(i); + _scope.define(ids[i], t, false, new ParmNode(ids[i],i+2,t,fun,con(t)).peephole(), loc); + } + + // Parse the body + Node last=ZERO; + while (!peek('}') && !_lexer.isEOF()) + last = parseStatement(); + + // Last expression is the return + if( ctrl()._type==Type.CONTROL ) + fun.addReturn(ctrl(), _scope.mem().merge(), last); + + // Pop off the inProgress node on the multi-exit Region merge + assert r.inProgress(); + r ._inputs.pop(); + rmem._inputs.pop(); + rrez._inputs.pop(); + r._loc = loc(); // Final position + assert !r.inProgress(); + + // Force peeps, which have been avoided due to inProgress + ret.setDef(1,rmem.peephole()); + ret.setDef(2,rrez.peephole()); + ret.setDef(0,r.peephole()); + ret = (ReturnNode)ret.peephole(); + + // Function scope ends + _scope.pop(); + _fun = oldfun; + _breakScope = breakScope; + _continueScope = continueScope; + // Reset control and memory to pre-function parsing days + ctrl(oldctrl.unkeep()); + _scope.mem(oldmem.unkeep()); + + return ret; + } + + /** + * Parses a block + * + * <pre> + * '{' statements '}' + * </pre> + * Does not parse the opening or closing '{}' + * @return a {@link Node} or {@code null} + */ + private Node parseBlock(ScopeNode.Kind kind) { + // Enter a new scope + _scope.push(kind); + Node last = ZERO; + while (!peek('}') && !_lexer.isEOF()) + last = parseStatement(); + // Exit scope + last.keep(); + _scope.pop(); + return last.unkeep(); + } + + /** + * Parses a statement + * + * <pre> + * returnStatement | declStatement | blockStatement | ifStatement | expressionStatement + * </pre> + * @return a {@link Node} or {@code null} + */ + private Node parseStatement() { + if( false ) return null; + else if (matchx("return") ) return parseReturn(); + 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(); + else if (matchx("#showGraph")) return require(showGraph(),";"); + else if (matchx(";") ) return null; // Empty statement + // Break ambiguity around leading function types and starting a block + else if (peek('{') && !isTypeFun() ) { + match("{"); + return require(parseBlock(ScopeNode.Kind.Block),"}"); + } + // Declaration or normal assignment/expression + else return parseDeclarationStatement(); + } + + /** + * Parses a while statement + * + * <pre> + * while ( expression ) statement + * </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( test ) { + // body; + // next; + // } + // } + require("("); + _scope.push(ScopeNode.Kind.Block); // 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; + + // Loop region has two control inputs, the first is the entry + // point, and second is back edge that is set after loop is parsed + // (see end_loop() call below). Note that the absence of back edge is + // used as an indicator to switch off peepholes of the region and + // associated phis; see {@code inProgress()}. + + ctrl(new LoopNode(loc(),ctrl()).peephole()); // Note we set back edge to null here + + // At loop head, we clone the current Scope (this includes all + // names in every nesting level within the Scope). + // We create phis eagerly for all the names we find, see dup(). + + // Save the current scope as the loop head + ScopeNode head = _scope.keep(); + // Clone the head Scope to create a new Scope for the body. + // Create phis eagerly as part of cloning + _xScopes.push(_scope = _scope.dup(true)); // The true argument triggers creating phis + + // Parse predicate + var pred = peek(';') ? con(1) : parseAsgn(); + require( doFor ? ";" : ")" ); + + // IfNode takes current control and predicate + Node ifNode = new IfNode(ctrl(), pred.keep()).peephole(); + // Setup projection nodes + 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 + // the loop predicate. Note that body Scope is still our current scope. + ctrl(ifF); + _xScopes.push(_breakScope = _scope.dup()); + _breakScope.addGuards(ifF,pred,true); // Up-cast predicate + + // No continues yet + _continueScope = null; + + // Parse the true side, which corresponds to loop body + // Our current scope is the body Scope + ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection + _scope.addGuards(ifT,pred.unkeep(),false); // Up-cast predicate + parseStatement(); // Parse loop body + _scope.removeGuards(ifT); + + // Merge the loop bottom into other continue statements + if (_continueScope != null) { + _continueScope = jumpTo(_continueScope); + _scope.kill(); + _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, + // it sets the second input to the corresponding input from the back + // edge. If the phi is redundant, it is replaced by its sole input. + var exit = _breakScope; + head.endLoop(_scope, exit); + head.unkeep().kill(); + + _xScopes.pop(); // Cleanup + _xScopes.pop(); // Cleanup + + _continueScope = savedContinueScope; + _breakScope = savedBreakScope; + + // At exit the false control is the current control, and + // the scope is the exit scope after the exit test. + _xScopes.pop(); + _xScopes.push(exit); + _scope = exit; + return ZERO; + } + + private ScopeNode jumpTo(ScopeNode toScope) { + ScopeNode cur = _scope.dup(); + ctrl(XCTRL); // Kill current scope + // Prune nested lexical scopes that have depth > than the loop head + // We use _breakScope as a proxy for the loop head scope to obtain the depth + while( cur._lexSize.size() > _breakScope._lexSize.size() ) + cur.pop(); + // If this is a continue then first time the target is null + // So we just use the pruned current scope as the base for the + // "continue" + if( toScope == null ) + return cur; + // toScope is either the break scope, or a scope that was created here + assert toScope._lexSize.size() <= _breakScope._lexSize.size(); + toScope.ctrl(toScope.mergeScopes(cur,loc()).peephole()); + return toScope; + } + + private void checkLoopActive() { if (_breakScope == null) throw error("No active loop for a break or continue"); } + + private Node parseContinue() { checkLoopActive(); _continueScope = require(jumpTo( _continueScope ),";"); return ZERO; } + private Node parseBreak () { + checkLoopActive(); + // At the time of the break, and loop-exit conditions are only valid if + // they are ALSO valid at the break. It is the intersection of + // conditions here, not the union. + _breakScope.removeGuards(_breakScope.ctrl()); + _breakScope = require(jumpTo(_breakScope ),";"); + _breakScope.addGuards(_breakScope.ctrl(), null, false); + return ZERO; + } + + // Look for an unbalanced `)`, skipping balanced + private void 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 ) { + posT( pos() - 1 ); + return; // Leave the `)` behind + } + break; + case '(': paren ++; break; + default: break; + } + } + + + /** + * Parses a statement + * + * <pre> + * if ( expression ) statement [else statement] + * </pre> + * @return a {@link Node}, never {@code null} + */ + private Node parseIf() { + // 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 + Node ifNode = new IfNode(ctrl(), pred).peephole(); + // Setup projection nodes + Node ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + Node ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole().keep(); + // In if true branch, the ifT proj node becomes the ctrl + // But first clone the scope and set it as current + ScopeNode fScope = _scope.dup(); // Duplicate current scope + _xScopes.push(fScope); // For graph visualization we need all scopes + + // Parse the true side + ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection + _scope.addGuards(ifT,pred,false); // Up-cast predicate + Node lhs = (stmt ? parseStatement() : parseAsgn()).keep(); // Parse true-side + _scope.removeGuards(ifT); + + // See if a one-sided def was made: "if(pred) int x = 1;" and throw. + // See if any forward-refs were made, and copy them to the other side: + // "pred ? n*fact(n-1) : 1" + fScope.balanceIf(_scope); + + ScopeNode tScope = _scope; + + // Parse the false side + _scope = fScope; // Restore scope, then parse else block if any + ctrl(ifF.unkeep()); // Ctrl token is now set to ifFalse projection + // 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 = match(fside); + Node rhs = (doRHS + ? (stmt ? parseStatement() : parseAsgn()) + : con(lhs._type.makeZero())).keep(); + _scope.removeGuards(ifF); + if( doRHS ) + fScope = _scope; + pred.unkeep(); + + // Check the trinary widening int/flt + if( !stmt ) { + rhs = widenInt( rhs.unkeep(), lhs._type ).keep(); + lhs = widenInt( lhs.unkeep(), rhs._type ).keep(); + } + + _scope = tScope; + _xScopes.pop(); // Discard pushed from graph display + // See if a one-sided def was made: "if(pred) int x = 1;" and throw. + // See if any forward-refs were made, and copy them to the other side: + // "pred ? n*fact(n-1) : 1" + tScope.balanceIf(fScope); + + // Merge results + RegionNode r = ctrl(tScope.mergeScopes(fScope,loc())); + Node ret = peep(new PhiNode("",lhs._type.meet(rhs._type),r,lhs.unkeep(),rhs.unkeep())); + // Immediately fail e.g. `arg ? 7 : ptr` + ParseException err; + if( !stmt && (err=ret.err()) !=null ) throw err; + r.peephole(); + return ret; + } + + /** + * Parses a return statement; "return" already parsed. + * The $ctrl edge is killed. + * + * <pre> + * 'return' expr ; + * </pre> + * @return an expression {@link Node}, never {@code null} + */ + private Node parseReturn() { + var expr = require(parseAsgn(), ";"); + // Need default memory, since it can be lazy, need to force + // a non-lazy Phi + _fun.addReturn(ctrl(), _scope.mem().merge(), expr); + ctrl(XCTRL); // Kill control + return expr; + } + + /** + * Dumps out the node graph + * @return {@code null} + */ + Node showGraph() { + System.out.println(new GraphVisualizer().generateDotOutput(_code._stop,_scope,_xScopes)); + return null; + } + + /** Parse: [name '='] expr + */ + private Node parseAsgn() { + int old = pos(); + String name = _lexer.matchId(); + // Just a plain expression, no assignment. + // Distinguish `var==expr` from `var=expr` + if( name==null || KEYWORDS.contains(name) || !matchOpx('=','=') ) + { pos(old); return parseExpression(); } + + // Parse assignment expression + Node expr = parseAsgn(); + + // Final variable to update + Var def = _scope.lookup(name); + if( def==null ) + throw error("Undefined name '" + name + "'"); + + // TOP fields are for late-initialized fields; these have never + // been written to, and this must be the final write. Other writes + // outside the constructor need to check the final bit. + if( _scope.in(def._idx)._type!=Type.TOP && def._final && + // Inside a constructor, final assign is OK, outside nope + !(_scope.inCon() && def._idx >= _scope._lexSize.last()) ) + throw error("Cannot reassign final '"+name+"'"); + + // Lift expression, based on type + Node lift = liftExpr(expr.keep(), def.type(), def._final); + // Update + _scope.update(name,lift); + // Return un-lifted expr + return expr.unkeep(); + } + + // Make finals deep; widen ints to floats; narrow wide int types. + // Early error if types do not match variable. + private Node liftExpr( Node expr, Type t, boolean xfinal ) { + assert !(expr._type instanceof TypeMemPtr tmp) || !tmp.isFRef(); + // Final is deep on ptrs + if( xfinal && t instanceof TypeMemPtr tmp ) { + t = tmp.makeRO(); + expr = peep(new ReadOnlyNode(expr)); + } + // Auto-widen int to float + expr = widenInt( expr, t ); + // Auto-narrow wide ints to narrow ints + expr = zsMask(expr,t); + // Type is sane + if( expr._type!=Type.BOTTOM && !expr._type.shallowISA(t) ) + throw error("Type " + expr._type.str() + " is not of declared type " + t.str()); + return expr; + } + + private Node widenInt( Node expr, Type t ) { + return (expr._type instanceof TypeInteger || expr._type==Type.NIL) && t instanceof TypeFloat + ? peep(new ToFloatNode(expr)) : expr; + } + + /** + * Parse declaration or expression statement + * declStmt = type var['=' exprAsgn][, var['=' exprAsgn]]* ';' | exprAsgn ';' + * <p> + * exprAsgn = var '=' exprAsgn | expr + */ + private Node parseDeclarationStatement() { + Type t = type(); + if( t == null ) + return require(parseAsgn(),";"); + + // now parse var['=' asgnexpr] in a loop + Node n = parseDeclaration(t); + while( match(",") ) + n = parseDeclaration(t); + return require(n,";"); + } + + /** 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 hasBang = match("!"); + Lexer loc = loc(); + String name = requireId(); + // Optional initializing expression follows + boolean xfinal = false; + Node expr; + if( match("=") ) { + expr = parseAsgn(); + // TOP means val and val is always final + xfinal = (t==Type.TOP) || + // BOTTOM is var and var is always not-final + (t!=Type.BOTTOM && + // no Bang AND + !hasBang && + // not-null (expecting null to be set to not-null) + expr._type != Type.NIL && + // Pointers are final by default; int/flt are not-final by default. + (t instanceof TypeNil)); + // var/val, then type comes from expression + if( inferType ) { + if( expr._type==Type.NIL ) + throw error("a not-null/non-zero expression"); + t = expr._type; + if( !xfinal ) t = t.glb(); // Widen if not final + } + if( t instanceof TypeFunPtr && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) + _code.link(tfp)._name = name; + + } else { + // Need an expression to infer the type. + // Also, if not-null then need an initializing expression. + // Allowed in a class def, because type/init will happen in the constructor. + if( (inferType || (t instanceof TypeNil tn && !tn.nullable() )) && !_scope.inCon() ) + throw errorSyntax("=expression"); + // Initial value for uninitialized struct fields. + expr = switch( t ) { + // Nullable pointers get a NIL; not-null get a TOP which + // signals that they *must* be initialized in the constructor. + case TypeNil tn -> tn.nullable() ? NIL : con(Type.TOP); + case TypeInteger ti -> ZERO; + case TypeFloat tf -> con(TypeFloat.FZERO); + // Bottom signals type inference: they must be initialized in + // the constructor and that's when we'll discover the type. + case Type tt -> { assert tt==Type.BOTTOM; yield con(tt); } + }; + } + + // Lift expression, based on type + Node lift = liftExpr(expr, t, xfinal); + + if( xfinal && t instanceof TypeMemPtr tmp ) + t = tmp.makeRO(); + + // Lift type to the declaration. This will report as an error later if + // we cannot lift the type. + if( !lift._type.isa(t) ) + lift = peep(new CastNode(t,null,lift)); + // Define a new name + if( !_scope.define(name,t,xfinal,lift, loc) ) + throw error("Redefining name '" + name + "'", loc); + return lift; + } + + + + /** + * Parse a struct declaration, and return the following statement. + * Only allowed in top level scope. + * Structs cannot be redefined. + * + * @return The statement following the struct + */ + private Node parseStruct() { + if (_xScopes.size() > 1) throw error("struct declarations can only appear in top level scope"); + String typeName = requireId(); + Type t = TYPES.get(typeName); + if( t!=null && !(t instanceof TypeMemPtr tmp && tmp.isFRef() ) ) + throw error("struct '" + typeName + "' cannot be redefined"); + + // A Block scope parse, and inspect the scope afterward for fields. + _scope.push(ScopeNode.Kind.Constructor); + require("{"); + while (!peek('}') && !_lexer.isEOF()) + parseStatement(); + + // Grab the declarations and build fields and a Struct + int lexlen = _scope._lexSize.last(); + int varlen = _scope._vars._len; + StructNode s = new StructNode(); + Ary<Field> fs = new Ary<>(Field.class); + for( int i=lexlen; i<varlen; i++ ) { + s.addDef(_scope.in(i)); + Var v = _scope._vars.at(i); + if( !v.isFRef() ) // Promote to outer scope, not defined here + fs.push(Field.make(v._name,v.type(),_code.getALIAS(),v._final)); + } + TypeStruct ts = s._ts = TypeStruct.make(typeName, fs.asAry()); + TYPES.put(typeName, TypeMemPtr.make(ts)); + INITS.put(typeName,s.peephole().keep()); + // Done with struct/block scope + require("}"); + require(";"); + _scope.pop(); + return ZERO; + } + + + // Parse and return a type or null. Valid types always are followed by an + // 'id' which the caller must parse. This lets us distinguish forward ref + // 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 | val | var | struct[?] + private Type type() { + int old1 = pos(); + // Only type with a leading `{` is a function pointer... + if( peek('{') ) return typeFunPtr(); + + // Otherwise you get a type name + String tname = _lexer.matchId(); + if( tname==null ) return null; + + // Convert the type name to a type. + Type t0 = TYPES.get(tname); + // No new types as keywords + if( t0 == null && KEYWORDS.contains(tname) ) + return posT(old1); + if( t0 == Type.BOTTOM || t0 == Type.TOP ) return t0; // var/val type inference + Type t1 = t0 == null ? TypeMemPtr.make(TypeStruct.makeFRef(tname)) :t0; // Null: assume a forward ref type + // Nest arrays and '?' as needed + Type t2 = t1; + while( true ) { + if( match("?") ) { + if( !(t2 instanceof TypeMemPtr tmp) ) + throw error("Type "+t0+" cannot be null"); + if( tmp.nullable() ) throw error("Type "+t2+" already allows null"); + t2 = tmp.makeNullable(); + } else if( match("[]") ) { + t2 = typeAry(t2); + } else + break; + } + + // Check no forward ref + if( t0 != null ) return t2; + // Check valid forward ref, after parsing all the type extra bits. + // Cannot check earlier, because cannot find required 'id' until after "[]?" syntax + int old2 = pos(); + match("!"); + String id = _lexer.matchId(); + if( !(peek(',') || peek(';') || match("->")) ) + return posT(old1); + pos(old2); // Reset lexer to reparse + if( id==null ) + return posT(old1); // Reset lexer to reparse + // Yes a forward ref, so declare it + TYPES.put(tname,t1); + return t2; + } + + // Make an array type of t + private TypeMemPtr typeAry( Type t ) { + if( t instanceof TypeMemPtr tmp && tmp.notNull() ) + throw error("Arrays of reference types must always be nullable"); + String tname = "["+t.str()+"]"; + Type ta = TYPES.get(tname); + if( ta != null ) return (TypeMemPtr)ta; + // Need make an array type. + TypeStruct ts = TypeStruct.makeAry(TypeInteger.U32,_code.getALIAS(),t,_code.getALIAS()); + assert ts.str().equals(tname); + TypeMemPtr tary = TypeMemPtr.make(ts); + TYPES.put(tname,tary); + return tary; + } + + // A function type is `{ type... -> type }` or `{ type }`. + private Type typeFunPtr() { + int old = pos(); // Record lexer position + match("{"); // Skip already-peeked '{' + Type t0 = type(); // Either return or first arg + if( t0==null ) return posT(old); // Not a function + if( match("}") ) // No-arg function { -> type } + return TypeFunPtr.make(match("?"),TypeTuple.BOT,t0); + Ary<Type> ts = new Ary<>(Type.class); + ts.push(t0); // First argument + while( true ) { + if( match("->") ) { // End of arguments, parse return + Type ret = type(); + if( ret==null || !match("}") ) + return posT(old); // Not a function + return TypeFunPtr.make(match("?"),TypeTuple.make(ts.asAry()),ret); + } + Type t1 = type(); + if( t1==null ) return posT(old); // Not a function + ts.push(t1); + } + } + + // True if a TypeFunPtr, without advancing parser + private boolean isTypeFun() { + int old = pos(); + if( typeFunPtr()==null ) return false; + pos(old); + return true; + } + + /** + * Parse an expression of the form: + * + * <pre> + * expr : bitwise [? expr [: expr]] + * </pre> + * @return an expression {@link Node}, never {@code null} + */ + private Node parseExpression() { + Node expr = parseBitwise(); + return match("?") ? parseTrinary(expr,false,":") : expr; + } + + /** + * Parse an bitwise expression + * + * <pre> + * bitwise : compareExpr (('&' | '|' | '^') compareExpr)* + * </pre> + * @return a bitwise expression {@link Node}, never {@code null} + */ + private Node parseBitwise() { + Node lhs = parseComparison(); + while( true ) { + if( false ) ; + else if( match("&") ) lhs = new AndNode(loc(),lhs,null); + else if( match("|") ) lhs = new OrNode(loc(),lhs,null); + else if( match("^") ) lhs = new XorNode(loc(),lhs,null); + else break; + lhs.setDef(2,parseComparison()); + lhs = peep(lhs); + } + return lhs; + } + + /** + * Parse an expression of the form: + * + * <pre> + * expr : additiveExpr op additiveExpr + * </pre> + * @return an comparator expression {@link Node}, never {@code null} + */ + private Node parseComparison() { + var lhs = parseShift(); + while( true ) { + int idx=0; boolean negate=false; + // Test for any local nodes made, and "keep" lhs during peepholes + if( false ) ; + else if( match("==") ) { idx=2; lhs = new BoolNode.EQ(lhs, null); } + else if( match("!=") ) { idx=2; lhs = new BoolNode.EQ(lhs, null); negate=true; } + else if( match("<=") ) { idx=2; lhs = new BoolNode.LE(lhs, null); } + else if( match("<" ) ) { idx=2; lhs = new BoolNode.LT(lhs, null); } + else if( match(">=") ) { idx=1; lhs = new BoolNode.LE(null, lhs); } + else if( match(">" ) ) { idx=1; lhs = new BoolNode.LT(null, lhs); } + else break; + // Peepholes can fire, but lhs is already "hooked", kept alive + lhs.setDef(idx,parseShift()); + lhs = peep(lhs.widen()); + if( negate ) // Extra negate for != + lhs = peep(new NotNode(lhs)); + } + return lhs; + } + + /** + * Parse an additive expression + * + * <pre> + * shiftExpr : additiveExpr (('<<' | '>>' | '>>>') additiveExpr)* + * </pre> + * @return a shift expression {@link Node}, never {@code null} + */ + private Node parseShift() { + Node lhs = parseAddition(); + while( true ) { + if( false ) ; + else if( match("<<") ) lhs = new ShlNode(loc(),lhs,null); + else if( match(">>>")) lhs = new ShrNode(loc(),lhs,null); + else if( match(">>") ) lhs = new SarNode(loc(),lhs,null); + else break; + lhs.setDef(2,parseAddition()); + ParseException err; + if( (err=lhs.err()) != null ) + throw err; + lhs = peep(lhs.widen()); + } + return lhs; + } + + /** + * Parse an additive expression + * + * <pre> + * additiveExpr : multiplicativeExpr (('+' | '-') multiplicativeExpr)* + * </pre> + * @return an add expression {@link Node}, never {@code null} + */ + private Node parseAddition() { + Node lhs = parseMultiplication(); + while( true ) { + if( false ) ; + else if( match("+") ) lhs = new AddNode(lhs,null); + else if( match("-") ) lhs = new SubNode(lhs,null); + else break; + lhs.setDef(2,parseMultiplication()); + lhs = peep(lhs.widen()); + } + return lhs; + } + + /** + * Parse an multiplicativeExpr expression + * + * <pre> + * multiplicativeExpr : unaryExpr (('*' | '/') unaryExpr)* + * </pre> + * @return a multiply expression {@link Node}, never {@code null} + */ + private Node parseMultiplication() { + var lhs = parseUnary(); + while( true ) { + if( false ) ; + else if( match("*") ) lhs = new MulNode(lhs,null); + else if( match("/") ) lhs = new DivNode(lhs,null); + else break; + lhs.setDef(2,parseUnary()); + lhs = peep(lhs.widen()); + } + return lhs; + } + + /** + * Parse a unary minus expression. + * + * <pre> + * unaryExpr : ('-') unaryExpr | '!') unaryExpr | postfixExpr | primaryExpr | '--' Id | '++' Id + * </pre> + * @return a unary expression {@link Node}, never {@code null} + */ + private Node 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 ) { + Var n = _scope.lookup(name); + if( n != null && !(n.type() instanceof TypeMemPtr) ) { + if( n._final ) + throw error("Cannot reassign final '"+n._name+"'"); + Node expr = n.type() instanceof TypeFloat + ? peep(new AddFNode(_scope.in(n),con(TypeFloat.constant(delta)))) + : 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()); + } + + /** + * Parse a primary expression: + * + * <pre> + * primaryExpr : integerLiteral | true | false | null | new Type | '(' expression ')' | Id['++','--'] + * </pre> + * @return a primary {@link Node}, never {@code null} + */ + private Node parsePrimary() { + if( _lexer.isNumber(_lexer.peek()) ) return parseLiteral(); + if( matchx("true" ) ) return con(1); + if( matchx("false") ) return ZERO; + if( matchx("null" ) ) return NIL; + if( match ("(" ) ) return require(parseAsgn(), ")"); + if( matchx("new" ) ) return alloc(); + if( match ("{" ) ) return require(func(),"}"); + // Expect an identifier now + Var n = requireLookupId(); + Node rvalue = _scope.in(n); + if( rvalue._type == Type.BOTTOM ) + if( rvalue instanceof FRefNode ) return rvalue; + else throw error("Cannot read uninitialized field '"+n._name+"'"); + // Check for assign-update, x += e0; + char ch = _lexer.matchOperAssign(); + if( ch==0 ) return rvalue; + Node op = opAssign(ch,rvalue,n._final,n.type(),n._name); + _scope.update(n,op); + return postfix(ch) ? rvalue.unkeep() : op; + } + + // Check for assign-update, "x += e0". + // Returns "x + e0"; the caller assigns value. + // if ch is postfix, then lhs is kept and caller will unkeep and return it. + private Node opAssign(char ch, Node lhs, boolean xfinal, Type t, String name ) { + if( xfinal ) + throw error("Cannot reassign final '"+name+"'"); + // RHS of the update. + lhs.keep(); // Alive across parseAsgn + Node rhs = + (byte)ch == 1 ? con( 1) : // var++ + (byte)ch == -1 ? con(-1) : // var-- + parseAsgn(); // var op= rhs + if( !postfix(ch) ) lhs.unkeep(); // Allow to die in next peep + // 4 cases: + // int + int ==>> narrow int + // int + flt ==>> error, caller must fail assigning flt into int + // flt + int ==>> use float op, wrap toFloat() + // 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(); + }; + // Convert to float ops, or narrow int types; error if not declared type. + // Also, if postfix LHS is still keep() + return liftExpr(peep(op.widen()),t,false); + } + + + // Expect an ID here. If not found, assume a forward reference function + Var requireLookupId() { + String id = _lexer.matchId(); + if( id == null || KEYWORDS.contains(id) ) + throw errorSyntax("an identifier or expression"); + Var n = _scope.lookup(id); + // If missing, assume a forward reference + if( n==null ) { + _scope.define(id, FRefNode.FREF_TYPE, true, XCTRL, loc()); + n = _scope.lookup(id); + } else { + // Lookup worked on an out-of-function value. + if( _scope.outOfFunction(n) ) { + // No closures, so this has to be a final constant (which + // includes forward refs) + Node def = _scope.in(n._idx); + if( !(def instanceof FRefNode) && !(n._final && def._type.isConstant()) ) + throw error("Variable '"+n._name+"' is out of function scope and must be a final constant"); + } + } + return n; + } + + /** + Parse an allocation + */ + private Node alloc() { + Type t = type(); + if( t==null ) throw error("Expected a type"); + // Parse ary[ length_expr ] + if( match("[") ) { + if( !t.makeZero().isa(t) ) + throw error("Cannot allocate a non-nullable, since arrays always zero/null fill"); + Node len = parseAsgn(); + if( !(len._type instanceof TypeInteger) ) + throw error("Cannot allocate an array with length "+len._type); + require("]"); + TypeMemPtr tmp = typeAry(t); + return newArray(tmp._obj,len); + } + + if( !(t instanceof TypeMemPtr tmp) ) + throw error("Cannot allocate a "+t.str()); + + // Parse new struct { default_initialization } + StructNode s = INITS.get(tmp._obj._name); + if( s==null ) throw error("Unknown struct type '" + tmp._obj._name + "'"); + + Field[] fs = s._ts._fields; + // if the object is fully initialized, we can skip a block here. + // Check for constructor block: + boolean hasConstructor = match("{"); + Ary<Node> init=s._inputs; int idx=0; + if( hasConstructor ) { + idx = _scope.nIns(); + // Push a scope, and pre-assign all struct fields. + _scope.push(ScopeNode.Kind.Block); + Lexer loc = loc(); + for( int i=0; i<fs.length; i++ ) + _scope.define(fs[i]._fname, fs[i]._type, fs[i]._final, s.in(i)._type==Type.TOP ? con(Type.BOTTOM) : s.in(i), loc); + // Parse the constructor body + require(parseBlock(ScopeNode.Kind.Constructor),"}"); + init = _scope._inputs; + } + // Check that all fields are initialized + for( int i=idx; i<init.size(); i++ ) + if( init.at(i)._type == Type.TOP || init.at(i)._type == Type.BOTTOM ) + throw error("'"+tmp._obj._name+"' is not fully initialized, field '" + fs[i-idx]._fname + "' needs to be set in a constructor"); + Node ptr = newStruct(tmp._obj, con(tmp._obj.offset(fs.length)), idx, init ); + if( hasConstructor ) + _scope.pop(); + return ptr; + } + + + /** + * Return a NewNode initialized memory. + * @param obj is the declared type, with GLB fields + * @param init is a collection of initialized fields + */ + private Node newStruct( TypeStruct obj, Node size, int idx, Ary<Node> init ) { + Field[] fs = obj._fields; + if( fs==null ) + throw error("Unknown struct type '" + obj._name + "'"); + int len = fs.length; + Node[] ns = new Node[2+len]; + ns[0] = ctrl(); // Control in slot 0 + // Total allocated length in bytes + ns[1] = size; + // Memory aliases for every field + for( int i = 0; i < len; i++ ) + ns[2+i] = memAlias(fs[i]._alias); + Node nnn = new NewNode(TypeMemPtr.make(obj), ns).peephole().keep(); + for( int i = 0; i < len; i++ ) + memAlias(fs[i]._alias, new ProjNode(nnn,i+2,memName(fs[i]._alias)).peephole()); + Node ptr = new ProjNode(nnn.unkeep(),1,obj._name).peephole().keep(); + + // Initial nonzero values for every field + for( int i = 0; i < len; i++ ) { + Node val = init.get( i + idx ); + if( val._type != val._type.makeZero() ) { + Node mem = memAlias(fs[i]._alias); + Node st = new StoreNode(loc(),fs[i]._fname,fs[i]._alias,fs[i]._type,mem,ptr,con(obj.offset(i)),val,true).peephole(); + memAlias(fs[i]._alias,st); + } + } + + return ptr.unkeep(); + } + + private static final Ary<Node> ALTMP = new Ary<>(Node.class); + private Node newArray(TypeStruct ary, Node len) { + int base = ary.aryBase (); + int scale= ary.aryScale(); + Node size = peep(new AddNode(con(base),peep(new ShlNode(null,len.keep(),con(scale))))); + ALTMP.clear(); ALTMP.add(len.unkeep()); ALTMP.add(con(ary._fields[1]._type.makeZero())); + return newStruct(ary,size,0,ALTMP); + } + + // We set up memory aliases by inserting special vars in the scope these + // variables are prefixed by $ so they cannot be referenced in Simple code. + // Using vars has the benefit that all the existing machinery of scoping + // and phis work as expected + private Node memAlias(int alias ) { return _scope.mem(alias ); } + private void memAlias(int alias, Node st) { _scope.mem(alias, st); } + public static String memName(int alias) { return ("$"+alias).intern(); } + + /** + * Parse postfix expression; this can be a field expression, an array + * lookup or a postfix operator like '#' + * + * <pre> + * expr ('.' FIELD)* [ = expr ] // Field reference + * expr '#' // Postfix unary read operator + * expr ['++' | '--' ] // Postfix unary write operator + * expr ('[' expr ']')* = [ = expr ] // Array reference + * expr '(' [args,]* ')' // Function call + * </pre> + */ + private Node parsePostfix(Node expr) { + String name; + if( match(".") ) name = requireId(); + else if( match("#") ) name = "#"; + else if( match("[") ) name = "[]"; + else if( match("(") ) return parsePostfix(require(functionCall(expr),")")); + else return expr; // No postfix + + if( expr._type==Type.NIL ) + throw error("Accessing unknown field '" + name + "' from 'null'"); + + // Sanity check expr for being a reference + if( !(expr._type instanceof TypeMemPtr ptr) ) { + throw error( "Expected "+(name=="#" || name=="[]" ? "array" : "reference")+" but found " + expr._type.str() ); + } + + // Sanity check field name for existing + TypeMemPtr tmp = (TypeMemPtr)TYPES.get(ptr._obj._name); + if( tmp == null ) throw error("Accessing unknown field '" + name + "' from '" + ptr + "'"); + TypeStruct base = tmp._obj; + int fidx = base.find(name); + if( fidx == -1 ) throw error("Accessing unknown field '" + name + "' from '" + ptr.str() + "'"); + + // Get field type and layout offset from base type and field index fidx + Field f = base._fields[fidx]; // Field from field index + Type tf = f._type; + if( tf instanceof TypeMemPtr ftmp && ftmp.isFRef() ) + tf = ftmp.makeFrom(((TypeMemPtr)(TYPES.get(ftmp._obj._name)))._obj); + + // Field offset; fixed for structs, computed for arrays + Node off = (name.equals("[]") // If field is an array body + // Array index math + ? peep(new AddNode(con(base.aryBase()),peep(new ShlNode(null,require(parseAsgn(),"]"),con(base.aryScale()))))) + // Struct field offsets are hardwired + : con(base.offset(fidx))).keep(); + + // Disambiguate "obj.fld==x" boolean test from "obj.fld=x" field assignment + if( matchOpx('=','=') ) { + Node val = parseAsgn().keep(); + Node lift = liftExpr( val, tf, f._final ); + + Node st = new StoreNode(loc(), name, f._alias, tf, memAlias(f._alias), expr, off.unkeep(), lift, false); + // Arrays include control, as a proxy for a safety range check. + // Structs don't need this; they only need a NPE check which is + // done via the type system. + if( base.isAry() ) st.setDef(0,ctrl()); + memAlias(f._alias, st.peephole()); + return val.unkeep(); // "obj.a = expr" returns the expression while updating memory + } + + Node load = new LoadNode(loc(),name, f._alias, tf, memAlias(f._alias), expr.keep(), off); + // Arrays include control, as a proxy for a safety range check + // Structs don't need this; they only need a NPE check which is + // done via the type system. + if( base.isAry() && !name.equals("#") ) load.setDef(0,ctrl()); + load = peep(load); + + // Check for assign-update, "ptr.fld += expr" or "ary[idx]++" + char ch = _lexer.matchOperAssign(); + if( ch!=0 ) { + Node op = opAssign(ch,load,f._final,tf,name); + Node st = new StoreNode(loc(), name, f._alias, tf, memAlias(f._alias), expr.unkeep(), off, op, false); + // Arrays include control, as a proxy for a safety range check. + // Structs don't need this; they only need a NPE check which is + // done via the type system. + if( base.isAry() ) st.setDef(0,ctrl()); + memAlias(f._alias, peep(st)); + load = postfix(ch) ? load.unkeep() : op; + // And use the original loaded value as the result + } else expr.unkill(); + off.unkill(); + + return parsePostfix(load); + } + + + // zero/sign extend. "i" is limited to either classic unsigned (min==0) or + // classic signed (min=minus-power-of-2); max=power-of-2-minus-1. + private Node zsMask(Node val, Type t ) { + if( !(val._type instanceof TypeInteger tval && t instanceof TypeInteger t0 && !tval.isa(t0)) ) { + if( !(val._type instanceof TypeFloat tval && t instanceof TypeFloat t0 && !tval.isa(t0)) ) + return val; + // Float rounding + return peep(new RoundF32Node(val)); + } + if( t0._min==0 ) // Unsigned + return peep(new AndNode(null,val,con(t0._max))); + // Signed extension + int shift = Long.numberOfLeadingZeros(t0._max)-1; + Node shf = con(shift); + if( shf._type==TypeInteger.ZERO ) + return val; + return peep(new SarNode(null,peep(new ShlNode(null,val,shf.keep())),shf.unkeep())); + } + + /** + * Parse a function body; the caller will parse the surrounding "{}" + * + * <pre> + * { [type arg,]* -> expr } + * { expr } // The no-argument function + * </pre> + */ + private Node func() { + Ary<Type> ts = new Ary<>(Type.class); + Ary<String> ids = new Ary<>(String.class); + _lexer.skipWhiteSpace(); + Lexer loc = loc(); // First argument location + while( true ) { + Type t = type(); // Arg type + if( t==null ) break; + String id = requireId(); + ts .push(t ); // Push type/arg pairs + ids.push(id); + match(","); + } + require("->"); + // Make a concrete function type, with a fidx + TypeFunPtr tfp = _code.makeFun(TypeTuple.make(ts.asAry()),Type.BOTTOM); + ReturnNode ret = parseFunctionBody(tfp,loc,ids.asAry()); + return con(ret._fun.sig()); + } + + /** + * Parse function call arguments; caller will parse the surrounding "()" + * <pre> + * ( arg* ) + * </pre> + */ + private Node functionCall(Node expr) { + if( expr._type == Type.NIL ) + throw error("Calling a null function pointer"); + if( !(expr instanceof FRefNode) && !expr._type.isa(TypeFunPtr.BOT) ) + throw error("Expected a function but got "+expr._type.glb().str()); + expr.keep(); // Keep while parsing args + + Ary<Node> args = new Ary<Node>(Node.class); + args.push(null); // Space for ctrl,mem + args.push(null); + while( !peek(')') ) { + Node arg = parseAsgn(); + if( arg==null ) break; + args.push(arg.keep()); + if( !match(",") ) break; + } + // Control & memory after parsing args + args.set(0,ctrl().keep()); + args.set(1,_scope.mem().merge().keep()); + args.push(expr); // Function pointer + // Unkeep them all + for( Node arg : args ) + arg.unkeep(); + // Dead into the call? Skip all the node gen + if( ctrl()._type == Type.XCONTROL ) { + for( Node arg : args ) + if( arg.isUnused() ) + arg.kill(); + return con(Type.TOP); + } + + // Into the call + CallNode call = (CallNode)new CallNode(loc(), args.asAry()).peephole(); + + // Post-call setup + CallEndNode cend = (CallEndNode)new CallEndNode(call).peephole(); + // Control from CallEnd + ctrl(new CProjNode(cend,0,ScopeNode.CTRL).peephole()); + // Memory from CallEnd + MemMergeNode mem = new MemMergeNode(true); + mem.addDef(null); // Alias#0 + mem.addDef(new ProjNode(cend,1,ScopeNode.MEM0).peephole()); + _scope.mem(mem); + // Call result + return new ProjNode(cend,2,null).peephole(); + } + + /** + * Parse integer literal + * + * <pre> + * integerLiteral: [1-9][0-9]* | [0] + * floatLiteral: [digits].[digits]?[e [digits]]? + * </pre> + */ + private ConstantNode parseLiteral() { return con(_lexer.parseNumber()); } + public static Node con( long con ) { return con==0 ? ZERO : con(TypeInteger.constant(con)); } + public static ConstantNode con( Type t ) { return (ConstantNode)new ConstantNode(t).peephole(); } + public Node peep( Node n ) { + // Peephole, then improve with lexically scoped guards + return _scope.upcastGuard(n.peephole()); + } + + ////////////////////////////////// + // Utilities for lexical analysis + + // Return true and skip if "syntax" is next in the stream. + private boolean match (String syntax) { return _lexer.match (syntax); } + // Match must be "exact", not be followed by more id letters + private boolean matchx(String syntax) { return _lexer.matchx(syntax); } + private boolean matchOpx(char c0, char c1) { return _lexer.matchOpx(c0,c1); } + // Return true and do NOT skip if 'ch' is next + private boolean peek(char ch) { return _lexer.peek(ch); } + private boolean peekIsId() { return _lexer.peekIsId(); } + + // ch is +/- 1, means oper++ or oper-- means postfix + private static boolean postfix(char ch) { + return (byte)ch== 1 || (byte)ch== -1; + } + + public int pos() { return _lexer._position; } + private int pos(int pos) { + int old = _lexer._position; + _lexer._position = pos; + return old; + } + private Type posT(int pos) { _lexer._position = pos; return null; } + // Source code location + Lexer loc() { return new Lexer(_lexer); } + + + // Require and return an identifier + private String requireId() { + String id = _lexer.matchId(); + if (id != null && !KEYWORDS.contains(id) ) return id.intern(); + throw error("Expected an identifier, found '"+id+"'"); + } + + private String matchId() { + int old = pos(); + String id = _lexer.matchId(); + if( id==null ) return null; + if( !KEYWORDS.contains(id) ) return id; + pos(old); + return null; + } + + // Require an exact match + private Parser require(String syntax) { require(null, syntax); return this; } + private <N> N require(N n, String syntax) { + if (match(syntax)) return n; + throw errorSyntax(syntax); + } + + //////////////////////////////////// + // Lexer components + + // Lexer provides low level access to the raw file bytes, peeks and matches + // short strings, parses numbers, skips comments and whitespace, tracks + // line numbers, allows the parse position to be saved and restored, and + // serves as a location indicator for errors. + + public static class Lexer { + + // Input buffer; an array of text bytes read from a file or a string + private final byte[] _input; + // Tracks current position in input buffer + private int _position = 0; + + // + private int _line_number = 1; + // Start of current line + private int _line_start = 0; + + /** + * Record the source text for lexing + */ + public Lexer(String source) { + this(source.getBytes()); + } + + /** + * Direct from disk file source + */ + public Lexer(byte[] buf) { + _input = buf; + } + + /** + * Copy a lexer from a lexer + */ + private Lexer(Lexer l) { + _input = l._input; + _position = l._position; + _line_number = l._line_number; + _line_start = l._line_start; + } + + // Very handy in the debugger, shows the unparsed program + @Override + public String toString() { + return new String(_input, _position, _input.length - _position); + } + + // True if at EOF + private boolean isEOF() { + return _position >= _input.length; + } + + // Peek next character, or report EOF + private char peek() { + return isEOF() ? Character.MAX_VALUE // Special value that causes parsing to terminate + : (char) _input[_position]; + } + // Just crash if misused + public byte peek(int off) { return _input[_position+off]; } + + private void inc() { + if( _position++ < _input.length && _input[_position-1] == '\n' ) { + _line_number++; + _line_start = _position; + } + } + // Does not honor LF, so caller must roll back position on a LF + private char nextChar() { + char ch = peek(); + inc(); + return ch; + } + + // True if a white space + private boolean isWhiteSpace() { + return peek() <= ' '; // Includes all the use space, tab, newline, CR + } + + /** + * Return the next non-white-space character + */ + private void skipWhiteSpace() { + while( true ) { + if( isWhiteSpace() ) inc(); + // Skip // to end of line + else if( _position+2 < _input.length && + _input[_position ] == '/' && + _input[_position+1] == '/') { + inc(); inc(); + while( !isEOF() && _input[_position] != '\n' ) inc(); + } else break; + } + } + + // Next non-white-space character, or EOF + public char nextXChar() { skipWhiteSpace(); return nextChar(); } + + // Return true, if we find "syntax" after skipping white space; also + // then advance the cursor past syntax. + // Return false otherwise, and do not advance the cursor. + boolean match(String syntax) { + assert syntax.indexOf('\n')==-1; // No newlines in match + skipWhiteSpace(); + int len = syntax.length(); + if (_position + len > _input.length) return false; + for (int i = 0; i < len; i++) + if ((char) _input[_position + i] != syntax.charAt(i)) + return false; + _position += len; + return true; + } + + // Match must be exact and not followed by more ID characters. + // Prevents identifier "ifxy" from matching an "if" statement. + boolean matchx(String syntax) { + if( !match(syntax) ) return false; + if( !isIdLetter(peek()) ) return true; + _position -= syntax.length(); + return false; + } + boolean matchOpx(char c0, char c1) { + skipWhiteSpace(); + if( _position+1 >= _input.length || _input[_position]!=c0 || _input[_position+1]==c1 ) + return false; + inc(); + return true; + } + + private boolean peek(char ch) { + skipWhiteSpace(); + return peek()==ch; + } + + boolean peekIsId() { + skipWhiteSpace(); + return isIdStart(peek()); + } + + // Return an identifier or null + String matchId() { + return peekIsId() ? parseId() : null; + } + + // Used for errors + String getAnyNextToken() { + if (isEOF()) return ""; + if (isIdStart(peek())) return parseId(); + if (isNumber(peek())) return parseNumberString(); + if (isPunctuation(peek())) return parsePunctuation(); + return String.valueOf(peek()); + } + + + boolean isNumber(char ch) {return Character.isDigit(ch);} + + // Return a constant Type, either TypeInteger or TypeFloat + private Type parseNumber() { + int old = _position; + int len = isLongOrDouble(); + if( len > 0 ) { + if( len > 1 && _input[old]=='0' ) + throw error("Syntax error: integer values cannot start with '0'",this); + long i = Long.parseLong(new String(_input,old,len)); + return TypeInteger.constant(i); + } + return TypeFloat.constant(Double.parseDouble(new String(_input,old,-len))); + } + private String parseNumberString() { + int old = _position; + int len = Math.abs(isLongOrDouble()); + return new String(_input,old,len); + } + + // Return +len that ends a long + // Return -len that ends a double + private int isLongOrDouble() { + int old = _position; + char c; + while( Character.isDigit(c=nextChar()) ) ; + if( !(c=='e' || c=='.') ) + return --_position - old; + while( Character.isDigit(c=nextChar()) || c=='e' || c=='.' ) ; + return -(--_position - old); + } + + // First letter of an identifier + private boolean isIdStart(char ch) { + return Character.isAlphabetic(ch) || ch == '_'; + } + + // All characters of an identifier, e.g. "_x123" + private boolean isIdLetter(char ch) { + return Character.isLetterOrDigit(ch) || ch == '_'; + } + + private String parseId() { + int start = _position; + while (isIdLetter(nextChar())) ; + return new String(_input, start, --_position - start); + } + + // + private boolean isPunctuation(char ch) { + return "=;[]<>()+-/*&|^".indexOf(ch) != -1; + } + + private String parsePunctuation() { + int start = _position; + return new String(_input, start, 1); + } + + // Next oper= character, or 0. + // As a convenience, mark "++" as a char 1 and "--" as char -1 (65535) + public char matchOperAssign() { + skipWhiteSpace(); + 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; + } + } + + ParseException errorSyntax(String syntax) { return _errorSyntax("expected "+syntax); } + private ParseException _errorSyntax(String msg) { + return error("Syntax error, "+msg+": " + _lexer.getAnyNextToken()); + } + ParseException error(String msg) { return error(msg,_lexer); } + public static ParseException error(String msg, Lexer loc) { return new ParseException(msg,loc); } + + public static class ParseException extends RuntimeException { + public final Lexer _loc; + // file:line:charoff err + //String msg = "src:"+_line_number+":"+(_position-_line_start)+" "+errorMessage; + ParseException( String msg, Lexer loc ) { super(msg); _loc = loc; assert loc!=null; } + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/RegMask.java b/chapter20/src/main/java/com/seaofnodes/simple/RegMask.java new file mode 100644 index 00000000..20be7157 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/RegMask.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple; + +import java.util.BitSet; + +// A "register mask" - 1 bit set for each allowed register. In addition "stack +// slot" registers may be allowed, effectively making the set infinite. +public class RegMask { + + public static final RegMask EMPTY = new RegMask(); + + final BitSet _bits; + + public RegMask(long x ) { _bits = BitSet.valueOf(new long[]{x}); } + public RegMask(long[] xs) { _bits = BitSet.valueOf( xs); } + // Internal constructor + RegMask() { _bits = new BitSet(); } + RegMask(BitSet bs) { _bits = bs; } + + public boolean get(int r) { return _bits.get(r); } + + // Defensive writable copy + public RegMaskRW copy() { return new RegMaskRW( (BitSet)_bits.clone() ); } + + boolean size1() { return _bits.cardinality()==1; } +} + +class RegMaskRW extends RegMask { + public RegMaskRW() { super(); } + public RegMaskRW(long x) { super(new long[]{x}); } + public RegMaskRW(long[] xs) { super(xs); } + public RegMaskRW(BitSet bs) { super(bs); } + public void set(int r) { _bits.set(r); } + public void clr(int r) { _bits.clear(r); } + public void set(int r, boolean b) { _bits.set(r,b); } + public RegMaskRW and( RegMask r ) { + _bits.and(r._bits); + return this; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/SB.java b/chapter20/src/main/java/com/seaofnodes/simple/SB.java new file mode 100644 index 00000000..7d580c21 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/SB.java @@ -0,0 +1,91 @@ +package com.seaofnodes.simple; + +/** Tight/tiny StringBuilder wrapper. + * Short short names on purpose; so they don't obscure the printing. + * Can't believe this wasn't done long, long ago. */ +public final class SB { + private final StringBuilder _sb; + private int _indent = 0; + public SB( ) { _sb = new StringBuilder( ); } + public SB(String s) { _sb = new StringBuilder(s); } + public SB p( String s ) { _sb.append(s); return this; } + public SB p( float s ) { + if( Float.isNaN(s) ) + _sb.append( "Float.NaN"); + else if( Float.isInfinite(s) ) { + _sb.append(s > 0 ? "Float.POSITIVE_INFINITY" : "Float.NEGATIVE_INFINITY"); + } else _sb.append(s); + return this; + } + public SB p( double s ) { + if( Double.isNaN(s) ) + _sb.append("Double.NaN"); + else if( Double.isInfinite(s) ) { + _sb.append(s > 0 ? "Double.POSITIVE_INFINITY" : "Double.NEGATIVE_INFINITY"); + } else _sb.append(s); + return this; + } + public SB p( char s ) { _sb.append(s); return this; } + public SB p( int s ) { _sb.append(s); return this; } + public SB p( long s ) { _sb.append(s); return this; } + public SB p( boolean s) { _sb.append(s); return this; } + // 4 hex digits + public SB hex4(int s) { + assert (s>>4*4)==0; // Fits in 16 bits + for( int i=0; i<4; i++ ) { + int digit = (s>>((3-i)*4)) & 0xf; + _sb.append((char)((digit <= 9 ? '0' : ('A'-10))+digit)); + } + return this; + } + // Fixed width field + public SB fix( int sz, String s ) { + assert sz >=0; + for( int i=0; i<sz; i++ ) + _sb.append( i < s.length() ? s.charAt(i) : ' '); + return this; + } + + // Not spelled "p" on purpose: too easy to accidentally say "p(1.0)" and + // suddenly call the autoboxed version. + public SB pobj( Object s ) { _sb.append(s.toString()); return this; } + public SB i( int d ) { for( int i=0; i<d+_indent; i++ ) p(" "); return this; } + public SB i( ) { return i(0); } + public SB ip(String s) { return i().p(s); } + public SB s() { _sb.append(' '); return this; } + + // Increase indentation + public SB ii( int i) { _indent += i; return this; } + public SB ii() { return ii(1); } + // Decrease indentation + public SB di( int i) { _indent -= i; return this; } + public SB di() { return di(1); } + + public SB nl( ) { return p(System.lineSeparator()); } + // Last printed a nl + public boolean was_nl() { + int len = _sb.length(); + String nl = System.lineSeparator(); + int nlen = nl.length(); + if( len < nlen ) return false; + for( int i=0; i<nlen; i++ ) + if( _sb.charAt(len-nlen+i)!=nl.charAt(i) ) + return false; + return true; + } + + // Delete last char. Useful when doing string-joins and JSON printing and an + // extra separater char needs to be removed: + // + // sb.p('['); + // for( Foo foo : foos ) + // sb.p(foo).p(','); + // sb.unchar().p(']'); // remove extra trailing comma + // + public SB unchar() { return unchar(1); } + public SB unchar(int x) { _sb.setLength(_sb.length()-x); return this; } + + public SB clear() { _sb.setLength(0); return this; } + public int len() { return _sb.length(); } + @Override public String toString() { return _sb.toString(); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/SimpleWebSocket.java b/chapter20/src/main/java/com/seaofnodes/simple/SimpleWebSocket.java new file mode 100644 index 00000000..1ce766ff --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/SimpleWebSocket.java @@ -0,0 +1,148 @@ +package com.seaofnodes.simple; + +import java.io.*; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +// Why-Oh-Why is this needed? + +// Pass in a URI and a port. +// Opens the client; client has to open a websocket at the given port. +// Connects, handshakes. +// Thereafter you can call get/put to swap frames with client. + +class SimpleWebSocket extends ServerSocket { + final InputStream _in; + final OutputStream _out; + + SimpleWebSocket(URI uri, int port) throws IOException, NoSuchAlgorithmException { + super(port); + // Launch client + java.awt.Desktop.getDesktop().browse(uri); + // Look for client; get in/out streams + Socket sock = accept(); + _in = sock. getInputStream(); + _out = sock.getOutputStream(); + + + // Establish web socket with handshake; easier with a buffered reader + BufferedReader br = new BufferedReader(new InputStreamReader(_in)); + String key = null; + while( br.ready() ) { + String line = br.readLine(); + if( line.startsWith("Sec-WebSocket-Key: ") ) + key = line.substring(19); + } + assert key!=null; + // Magic handshake + byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n" + + "Connection: Upgrade\r\n" + + "Upgrade: websocket\r\n" + + "Sec-WebSocket-Accept: " + + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-1").digest((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes( StandardCharsets.UTF_8 ))) + + "\r\n\r\n").getBytes( StandardCharsets.UTF_8 ); + _out.write(response,0,response.length); + } + + final byte[] _ins = new byte[1024]; + final byte[] _str = new byte[1024]; + final byte[] _key = new byte[4]; + int _x, _len; + private int rawget() { assert _x<_len; return _ins[_x++]&0xFF; } + + // TODO: Flip to Direct Byte Buffer, this is just a poor-mans version + private void flipRead() throws IOException { + // Move _x to _len to offset 0 + System.arraycopy(_ins,_x,_ins,0,(_len-_x)); + _len = _len-_x; + _x=0; + // Read more + int len = _in.read(_ins,_len,_ins.length-_len); + if( len == -1 ) + throw Utils.TODO(); // EOF + _len += len; + } + // Block until you read at least len + private void atLeast(int len) throws IOException { + if( _x+len <= _len ) return; + flipRead(); + } + + + // Classic read from browser client. Encoded, so decoded here + public String get() throws IOException { + atLeast(1); // Need 1 byte for opcode + int op = rawget(); + if( (op&0x80)!=0x80 ) // FIN bit not set? + throw Utils.TODO( "Multi-part from client: " + op ); // Multipart message + op &= 0x7F; // Strip FIN bit + switch( op&15) { + case 1: { + atLeast(5); // Min message length + // Message length + int len = rawget()-128; + if( len > 125 ) { + if( len > 126 ) + throw Utils.TODO(); // Multi-byte length + len = (rawget()<<8) | rawget(); + } + atLeast(len+4); // Rest of message + // Encoding key + _key[0] = (byte)rawget(); + _key[1] = (byte)rawget(); + _key[2] = (byte)rawget(); + _key[3] = (byte)rawget(); + + // Decode bytes + for( int i=0; i<len; i++ ) + _str[i] = (byte)(rawget() ^ _key[i & 3]); + + return new String(_str,0,len); + } + // Client closes + case 8: return null; + // Ping + case 9: + // Send a PONG back. TODO: echo PING data in the PONG + _out.write(0x8A); // FIN + PONG + _out.write(0x00); // LENGTH==0 + return get(); + // Not (yet) implemented: continuation frames, ping/pong + default: + throw Utils.TODO("Unexpected op from client: "+op); // Multi-part message + } + } + + // Classic put to browser client. No encoding. + public void put(String msg) throws IOException { + int len = msg.length(); + if( len <= 125 ) { + _out.write(129); // FIN, opcode 1 - whole text message + _out.write(len); + for( int i=0; i<msg.length(); i++ ) + _out.write(msg.charAt(i)); + return; + } + if( len > 65535 ) + throw Utils.TODO("MOAR LENGTH"); + _out.write(129); // FIN, opcode 1 - whole text message + _out.write(126); // Length is 16b + _out.write(len>>>8); + _out.write(len&255); + for( int i=0; i<msg.length(); i++ ) + _out.write(msg.charAt(i)); + } + + @Override public void close() throws IOException { + if( _out != null ) { + _out.write(0b10001000); + _out.flush(); + } + super.close(); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/Utils.java b/chapter20/src/main/java/com/seaofnodes/simple/Utils.java new file mode 100644 index 00000000..1346c3ea --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/Utils.java @@ -0,0 +1,53 @@ +package com.seaofnodes.simple; + +import java.util.ArrayList; + +public class Utils { + public static RuntimeException TODO() { return TODO("Not yet implemented"); } + public static RuntimeException TODO(String msg) { return new RuntimeException(msg); } + + /** + * Fast, constant-time, element removal. Does not preserve order + * + * @param array ArrayList to modify + * @param i element to be removed + * @return element removed + */ + public static <E> void del(ArrayList<E> array, int i) { + E last = array.removeLast(); + if (i < array.size()) array.set(i, last); + } + + /** + * Search a list for an element by reference + * + * @param ary List to search in + * @param x Object to be searched + * @return >= 0 on success, -1 on failure + */ + public static <E> int find( ArrayList<E> ary, E x ) { + for( int i=0; i<ary.size(); i++ ) + if( ary.get(i)==x ) + return i; + return -1; + } + /** + * Search a list for an element by reference + * + * @param ary List to search in + * @param x Object to be searched + * @return >= 0 on success, -1 on failure + */ + public static <E> int find( E[] ary, E x ) { + for( int i=0; i<ary.length; i++ ) + if( ary[i]==x ) + return i; + return -1; + } + + // Rotate a long, nice for hashes + public static long rot( long x, int n ) { return (x<<n) | (x>>>n); } + + // Fold a 64bit hash into 32 bits + public static int fold( long x ) { return (int)((x>>32) ^ x); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/Var.java b/chapter20/src/main/java/com/seaofnodes/simple/Var.java new file mode 100644 index 00000000..e71d7ef2 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/Var.java @@ -0,0 +1,55 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.type.*; + + +/** + * The tracked fields are now complex enough to deserve a array-of-structs layout + */ +public class Var { + + public final String _name; // Declared name + public int _idx; // index in containing scope + private Type _type; // Declared type + public boolean _final; // Final field + public Parser.Lexer _loc; // Source location + public boolean _fref; // Forward ref + + public Var(int idx, String name, Type type, boolean xfinal, Parser.Lexer loc ) { + this(idx,name,type,xfinal,loc,false); + } + public Var(int idx, String name, Type type, boolean xfinal, Parser.Lexer loc, boolean fref) { + _idx = idx; + _name = name; + _type = type; + _final = xfinal; + _loc = loc; + _fref = fref; + } + public Type type() { + if( !_type.isFRef() ) return _type; + // Update self to no longer use the forward ref type + Type def = Parser.TYPES.get(((TypeMemPtr)_type)._obj._name); + return (_type=_type.meet(def)); + } + public Type lazyGLB() { + Type t = type(); + return t instanceof TypeMemPtr ? t : t.glb(); + } + + // Forward reference variables (not types) must be BOTTOM and + // distinct from inferred variables + public boolean isFRef() { return _fref; } + + public void defFRef( Type type, boolean xfinal, Parser.Lexer loc ) { + assert isFRef() && xfinal; + _type = type; + _final = true; + _loc = loc; + _fref = false; + } + + @Override public String toString() { + return _type.toString()+(_final ? " ": " !")+_name; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/AddFNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/AddFNode.java new file mode 100644 index 00000000..7d4e2910 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/AddFNode.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; + +import java.util.BitSet; + +public class AddFNode extends Node { + public AddFNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "AddF"; } + + @Override public String glabel() { return "+"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("+"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + if (in(1)._type instanceof TypeFloat i0 && + in(2)._type instanceof TypeFloat i1) { + if (i0.isConstant() && i1.isConstant()) + return TypeFloat.constant(i0.value()+i1.value()); + } + return in(1)._type.meet(in(2)._type); + } + + @Override + public Node idealize() { + Node lhs = in(1); + Type t2 = in(2)._type; + + // Add of 0. + if ( t2.isConstant() && t2 instanceof TypeFloat i && i.value()==0 ) + return lhs; + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new AddFNode(lhs,rhs); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/AddNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/AddNode.java new file mode 100644 index 00000000..6b7f2418 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/AddNode.java @@ -0,0 +1,190 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.*; + +import java.util.BitSet; + +import static com.seaofnodes.simple.Parser.con; + +public class AddNode extends Node { + public AddNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "Add"; } + + @Override public String glabel() { return "+"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("+"), visited); + return sb.append(")"); + } + + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1.isHigh() || t2.isHigh() ) + return TypeInteger.TOP; + if( t1 instanceof TypeInteger i1 && + t2 instanceof TypeInteger i2 ) { + if (i1.isConstant() && i2.isConstant()) + return TypeInteger.constant(i1.value()+i2.value()); + // Fold ranges like {0-1} + {2-3} into {2-4}. + if( !overflow(i1._min,i2._min) && + !overflow(i1._max,i2._max) ) + return TypeInteger.make(i1._min+i2._min,i1._max+i2._max); + } + return TypeInteger.BOT; + } + + static boolean overflow( long x, long y ) { + if( (x ^ y ) < 0 ) return false; // unequal signs, never overflow + return (x ^ (x + y)) < 0; // sum has unequal signs, so overflow + } + @Override + public Node idealize () { + Node lhs = in(1); + Node rhs = in(2); + Type t2 = rhs._type; + + // Add of 0. We do not check for (0+x) because this will already + // canonicalize to (x+0) + if( t2 == TypeInteger.ZERO ) + return lhs; + + // Add of same to a multiply by 2 + if( lhs==rhs ) + return new ShlNode(null,lhs,con(1)); + + // Goal: a left-spine set of adds, with constants on the rhs (which then fold). + + // Move non-adds to RHS + if( !(lhs instanceof AddNode) && rhs instanceof AddNode ) + return swap12(); + + // x+(-y) becomes x-y + if( rhs instanceof MinusNode minus ) + return new SubNode(lhs,minus.in(1)); + + // Now we might see (add add non) or (add non non) or (add add add) but never (add non add) + + // Do we have x + (y + z) ? + // Swap to (x + y) + z + // Rotate (add add add) to remove the add on RHS + if( rhs instanceof AddNode add ) + return new AddNode(new AddNode(lhs,add.in(1)).peephole(), add.in(2)); + + // Now we might see (add add non) or (add non non) but never (add non add) nor (add add add) + if( !(lhs instanceof AddNode) ) + // Rotate; look for (add (phi cons) con/(phi cons)) + return spine_cmp(lhs,rhs,this) ? swap12() : phiCon(this,true); + + // Now we only see (add add non) + + // Dead data cycle; comes about from dead infinite loops. Do nothing, + // the loop will peep as dead after a bit. + if( lhs.in(1) == lhs ) + return null; + + // Do we have (x + con1) + con2? + // Replace with (x + (con1+con2) which then fold the constants + // lhs.in(2) is con1 here + // If lhs.in(2) is not a constant, we add ourselves as a dependency + // because if it later became a constant then we could make this + // transformation. + if( lhs.in(2).addDep(this)._type.isConstant() && rhs._type.isConstant() ) + return new AddNode(lhs.in(1),new AddNode(lhs.in(2),rhs).peephole()); + + + // Do we have ((x + (phi cons)) + con) ? + // Do we have ((x + (phi cons)) + (phi cons)) ? + // Push constant up through the phi: x + (phi con0+con0 con1+con1...) + Node phicon = phiCon(this,true); + if( phicon!=null ) return phicon; + + // Now we sort along the spine via rotates, to gather similar things together. + + // Do we rotate (x + y) + z + // into (x + z) + y ? + if( spine_cmp(lhs.in(2),rhs,this) ) + return new AddNode(new AddNode(lhs.in(1),rhs).peephole(),lhs.in(2)); + + return null; + } + + // Rotation is only valid for associative ops, e.g. Add, Mul, And, Or, Xor. + // Do we have ((phi cons)|(x + (phi cons)) + con|(phi cons)) ? + // Push constant up through the phi: x + (phi con0+con0 con1+con1...) + static Node phiCon(Node op, boolean rotate) { + Node lhs = op.in(1); + Node rhs = op.in(2); + if( rhs._type==TypeInteger.TOP ) return null; + // LHS is either a Phi of constants, or another op with Phi of constants + PhiNode lphi = pcon(lhs,op); + if( rotate && lphi==null && lhs.nIns() > 2 ) { + // Only valid to rotate constants if both are same associative ops + if( lhs.getClass() != op.getClass() ) return null; + lphi = pcon(lhs.in(2),op); // Will rotate with the Phi push + } + if( lphi==null ) return null; + if( lphi.region().nIns() <=2 ) return null; // Phi is collapsing + + // RHS is a constant or a Phi of constants + if( !(rhs instanceof ConstantNode) && pcon(rhs,op)==null ) + return null; + + // If both are Phis, must be same Region + if( rhs instanceof PhiNode && lphi.in(0) != rhs.in(0) ) + return null; + + // Note that this is the exact reverse of Phi pulling a common op down + // to reduce total op-count. We don't get in an endless push-up + // push-down peephole cycle because the constants all fold first. + Node[] ns = new Node[lphi.nIns()]; + ns[0] = lphi.in(0); + // Push constant up through the phi: x + (phi con0+con0 con1+con1...) + for( int i=1; i<ns.length; i++ ) + ns[i] = op.copy(lphi.in(i), rhs instanceof PhiNode ? rhs.in(i) : rhs).peephole(); + String label = lphi._label + (rhs instanceof PhiNode rphi ? rphi._label : ""); + Node phi = new PhiNode(label,lphi._declaredType,ns).peephole(); + // Rotate needs another op, otherwise just the phi + return lhs==lphi ? phi : op.copy(lhs.in(1),phi); + } + + /** + * Tests if the op is a phi and has all constant inputs. + * If not, returns null. + * If op is a phi, but its inputs are not all constants, then dep is added as + * a dependency to the phi's non-const input, because if later the input turn into a constant + * dep can make progress. + */ + static PhiNode pcon(Node op, Node dep) { + return op instanceof PhiNode phi && phi.allCons(dep) ? phi : null; + } + + // Compare two off-spine nodes and decide what order they should be in. + // Do we rotate ((x + hi) + lo) into ((x + lo) + hi) ? + // Generally constants always go right, then Phi-of-constants, then muls, then others. + // Ties with in a category sort by node ID. + // TRUE if swapping hi and lo. + static boolean spine_cmp( Node hi, Node lo, Node dep ) { + if( lo._type.isConstant() ) return false; + if( hi._type.isConstant() ) return true ; + + if( lo instanceof PhiNode lphi && lphi.region()._type==Type.XCONTROL ) return false; + if( hi instanceof PhiNode hphi && hphi.region()._type==Type.XCONTROL ) return false; + + if( lo instanceof PhiNode && lo.allCons(dep) ) return false; + if( hi instanceof PhiNode && hi.allCons(dep) ) return true ; + + if( lo instanceof PhiNode && !(hi instanceof PhiNode) ) return true; + if( hi instanceof PhiNode && !(lo instanceof PhiNode) ) return false; + + // Same category of "others" + return lo._nid > hi._nid; + } + + @Override Node copy(Node lhs, Node rhs) { return new AddNode(lhs,rhs); } + @Override Node copyF() { return new AddFNode(null,null); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/AndNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/AndNode.java new file mode 100644 index 00000000..36e3a949 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/AndNode.java @@ -0,0 +1,58 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +import java.util.BitSet; + +public class AndNode extends LogicalNode { + public AndNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs); } + + @Override public String label() { return "And"; } + @Override public String op() { return "&"; } + + @Override public String glabel() { return "&"; } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1.isHigh() || t2.isHigh() ) + return TypeInteger.TOP; + if( t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1 ) { + if( i0.isConstant() && i1.isConstant() ) + return TypeInteger.constant(i0.value()&i1.value()); + // Sharpen allowed bits if either value is narrowed + long mask = i0.mask() & i1.mask(); + return mask < 0 ? TypeInteger.BOT : TypeInteger.make(0,mask); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t1 = lhs._type; + Type t2 = rhs._type; + + // And of -1. We do not check for (-1&x) because this will already + // canonicalize to (x&-1) + if( t2.isConstant() && t2 instanceof TypeInteger i && i.value()==-1 ) + return lhs; + + // Move constants to RHS: con*arg becomes arg*con + if ( t1.isConstant() && !t2.isConstant() ) + return swap12(); + + // Do we have ((x & (phi cons)) & con) ? + // Do we have ((x & (phi cons)) & (phi cons)) ? + // Push constant up through the phi: x & (phi con0&con0 con1&con1...) + Node phicon = AddNode.phiCon(this,true); + if( phicon!=null ) return phicon; + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new AndNode(_loc,lhs,rhs); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/BoolNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/BoolNode.java new file mode 100644 index 00000000..84be11a9 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/BoolNode.java @@ -0,0 +1,134 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; +import static com.seaofnodes.simple.type.TypeInteger.*; + +abstract public class BoolNode extends Node { + + public BoolNode(Node lhs, Node rhs) { + super(null, lhs, rhs); + } + + abstract public String op(); // String opcode name + + @Override + public String label() { return getClass().getSimpleName(); } + + @Override + public String glabel() { return op(); } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append(op()), visited); + return sb.append(")"); + } + + @Override + public TypeInteger compute() { + Type t1 = in(1)._type; + Type t2 = in(2)._type; + // Exactly equals? + if( t1.isHigh() || t2.isHigh() ) + return BOOL.dual(); + if( in(1)==in(2) ) + // LT fails, both EQ and LE succeed + return this instanceof LT ? FALSE : TRUE; + if( t1 instanceof TypeInteger i1 && + t2 instanceof TypeInteger i2 ) + return doOp(i1,i2); + if( t1 instanceof TypeFloat f1 && + t2 instanceof TypeFloat f2 && + f1.isConstant() && f2.isConstant() ) + return doOp(f1.value(), f2.value()) ? TRUE : FALSE; + return BOOL; + } + + TypeInteger doOp(TypeInteger t1, TypeInteger t2) { throw Utils.TODO(); } + boolean doOp(double lhs, double rhs) { throw Utils.TODO(); } + Node copyF(Node lhs, Node rhs) { return null; } + + @Override + public Node idealize() { + // Compare of same + if( in(1)==in(2) ) + return this instanceof LT ? Parser.ZERO : new ConstantNode(TRUE); + + // Equals pushes constant to the right; 5==X becomes X==5. + if( this instanceof EQ ) { + if( !(in(2) instanceof ConstantNode) ) { + // con==noncon becomes noncon==con + if( in(1) instanceof ConstantNode || in(1)._nid > in(2)._nid ) + // Equals sorts by NID otherwise: non.high == non.low becomes non.low == non.high + return in(1)._type instanceof TypeFloat ? new EQF(in(2),in(1)) : new EQ(in(2),in(1)); + } + // Equals X==0 becomes a !X + if( (in(2)._type == ZERO || in(2)._type == Type.NIL) ) + return new NotNode(in(1)); + } + + // Do we have ((x * (phi cons)) * con) ? + // Do we have ((x * (phi cons)) * (phi cons)) ? + // Push constant up through the phi: x * (phi con0*con0 con1*con1...) + Node phicon = AddNode.phiCon(this,this instanceof EQ); + if( phicon!=null ) return phicon; + + return null; + } + + public static class EQ extends BoolNode { + public EQ(Node lhs, Node rhs) { super(lhs,rhs); } + public String op() { return "=="; } + TypeInteger doOp(TypeInteger i1, TypeInteger i2) { + if( i1==i2 && i1.isConstant() ) return TRUE; + if( i1._max < i2._min || i1._min > i2._max ) return FALSE; + return BOOL; + } + Node copy(Node lhs, Node rhs) { return new EQ(lhs,rhs); } + Node copyF() { return new EQF(null,null); } + } + public static class LT extends BoolNode { + public LT(Node lhs, Node rhs) { super(lhs,rhs); } + public String op() { return "<" ; } + public String glabel() { return "<"; } + TypeInteger doOp(TypeInteger i1, TypeInteger i2) { + if( i1._max < i2._min ) return TRUE; + if( i1._min >= i2._max ) return FALSE; + return BOOL; + } + Node copy(Node lhs, Node rhs) { return new LT(lhs,rhs); } + Node copyF() { return new LTF(null,null); } + } + public static class LE extends BoolNode { + public LE(Node lhs, Node rhs) { super(lhs,rhs); } + public String op() { return "<="; } + public String glabel() { return "<="; } + TypeInteger doOp(TypeInteger i1, TypeInteger i2) { + if( i1._max <= i2._min ) return TRUE; + if( i1._min > i2._max ) return FALSE; + return BOOL; + } + Node copy(Node lhs, Node rhs) { return new LE(lhs,rhs); } + Node copyF() { return new LEF(null,null); } + } + + public static class EQF extends EQ { public EQF(Node lhs, Node rhs) { super(lhs,rhs); } boolean doOp(double lhs, double rhs) { return lhs == rhs; } } + public static class LTF extends LT { public LTF(Node lhs, Node rhs) { super(lhs,rhs); } boolean doOp(double lhs, double rhs) { return lhs < rhs; } } + public static class LEF extends LE { public LEF(Node lhs, Node rhs) { super(lhs,rhs); } boolean doOp(double lhs, double rhs) { return lhs <= rhs; } } + + // Unsigned less that, for range checks. Not directly user writable. + public static class ULT extends BoolNode { + public ULT(Node lhs, Node rhs) { super(lhs,rhs); } + public String op() { return "u<" ; } + public String glabel() { return "u<"; } + TypeInteger doOp(TypeInteger i1, TypeInteger i2) { + if( Long.compareUnsigned(i1._max,i2._min) < 0 ) return TRUE; + if( Long.compareUnsigned(i1._min,i2._max) >= 0 ) return FALSE; + return BOOL; + } + Node copy(Node lhs, Node rhs) { return new ULT(lhs,rhs); } + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/CFGNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/CFGNode.java new file mode 100644 index 00000000..174725f5 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/CFGNode.java @@ -0,0 +1,162 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Ary; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; +import java.util.HashMap; +import java.util.HashSet; + +/** Control Flow Graph Nodes + * <p> + * CFG nodes have a immediate dominator depth (idepth) and a loop nesting + * depth(loop_depth). + * <p> + * idepth is computed lazily upon first request, and is valid even in the + * Parser, and is used by peepholes during parsing and afterward. + * <p> + * loop_depth is computed after optimization as part of scheduling. + * + */ +public abstract class CFGNode extends Node { + + public CFGNode(Node... nodes) { super(nodes); } + public CFGNode(CFGNode cfg) { + super(cfg); + _idepth = cfg._idepth; + _ltree = cfg._ltree; + _pre = cfg._pre; + } + + public CFGNode cfg(int idx) { return (CFGNode)in(idx); } + + // Block head is Start, Region, CProj, but not e.g. If, Return, Stop + public boolean blockHead() { return false; } + + // Get the one control following; error to call with more than one e.g. an + // IfNode or other multi-way branch. + public CFGNode uctrl() { + CFGNode c = null; + for( Node n : _outputs ) + if( n instanceof CFGNode cfg ) + { assert c==null; c = cfg; } + return c; + } + + + // ------------------------------------------------------------------------ + /** + * Immediate dominator tree depth, used to approximate a real IDOM during + * parsing where we do not have the whole program, and also peepholes + * change the CFG incrementally. + * <p> + * See {@link <a href="https://en.wikipedia.org/wiki/Dominator_(graph_theory)">...</a>} + */ + public int _idepth; + public int idepth() { return _idepth==0 ? (_idepth=idom().idepth()+1) : _idepth; } + + // Return the immediate dominator of this Node and compute dom tree depth. + public CFGNode idom(Node dep) { return cfg(0); } + public final CFGNode idom() { return idom(null); } + + // Return the LCA of two idoms + public CFGNode _idom(CFGNode rhs, Node dep) { + if( rhs==null ) return this; + CFGNode lhs = this; + while( lhs != rhs ) { + var comp = lhs.idepth() - rhs.idepth(); + if( comp >= 0 ) lhs = ((CFGNode)lhs.addDep(dep)).idom(); + if( comp <= 0 ) rhs = ((CFGNode)rhs.addDep(dep)).idom(); + } + return lhs; + } + + // Anti-dependence field support + public int _anti; // Per-CFG field to help find anti-deps + + // ------------------------------------------------------------------------ + // Loop nesting + public LoopNode loop() { return _ltree._head; } + public int loopDepth() { return _ltree==null ? 0 : _ltree.depth(); } + + LoopTree _ltree; + int _pre; // Pre-order numbers for loop tree finding + private static class LoopTree { + LoopTree _par; + final LoopNode _head; + int _depth; + LoopTree(LoopNode head) { _head = head; } + @Override public String toString() { return "LOOP"+_head._nid; } + int depth() { + return _depth==0 ? (_par==null ? 0 : (_depth = _par.depth()+1)) : _depth; + } + } + + // ------------------------------------------------------------------------ + // Tag all CFG Nodes with their containing LoopNode; LoopNodes themselves + // also refer to *their* containing LoopNode, as well as have their depth. + // Start is a LoopNode which contains all at depth 1. + public void buildLoopTree(StopNode stop) { + _ltree = stop._ltree = Parser.XCTRL._ltree = new LoopTree((StartNode)this); + _bltWalk(2,null,stop, new BitSet()); + } + int _bltWalk( int pre, FunNode fun, StopNode stop, BitSet post ) { + // Pre-walked? + if( _pre!=0 ) return pre; + _pre = pre++; + // Pre-walk + for( Node use : _outputs ) + if( use instanceof CFGNode usecfg && !skip( usecfg ) ) + pre = usecfg._bltWalk( pre, use instanceof FunNode fuse ? fuse : fun, stop, post ); + + // Post-order work: find innermost loop + LoopTree inner = null, ltree; + for( Node use : _outputs ) { + if( !(use instanceof CFGNode usecfg) ) continue; + if( skip(usecfg) ) continue; + if( usecfg._type == Type.XCONTROL || // Do not walk dead control + usecfg._type == TypeTuple.IF_NEITHER ) // Nor dead IFs + continue; + // Child visited but not post-visited? + if( !post.get(usecfg._nid) ) { + // Must be a backedge to a LoopNode then + ltree = usecfg._ltree = new LoopTree((LoopNode)usecfg); + } else { + // Take child's loop choice, which must exist + ltree = usecfg._ltree; + // If falling into a loop, use the target loop's parent instead + if( ltree._head == usecfg ) { + if( ltree._par == null ) + // This loop never had an If test choose to take its + // exit, i.e. it is a no-exit infinite loop. + ltree._par = ltree._head.forceExit(fun,stop)._ltree; + ltree = ltree._par; + } + } + // Sort inner loops. The decision point is some branch far removed + // from either loop head OR either backedge so requires pre-order + // numbers to figure out innermost. + if( inner == null ) { inner = ltree; continue; } + if( inner == ltree ) continue; // No change + LoopTree outer = ltree._head._pre > inner._head._pre ? inner : ltree; + inner = ltree._head._pre > inner._head._pre ? ltree : inner; + inner._par = outer; + } + // Set selected loop + if( inner!=null ) + _ltree = inner; + // Tag as post-walked + post.set(_nid); + return pre; + } + + boolean skip(CFGNode usecfg) { + // Only walk control users that are alive. + // Do not walk from a Call to linked Fun's. + return usecfg instanceof XCtrlNode || + (this instanceof CallNode && usecfg instanceof FunNode) || + (this instanceof ReturnNode && usecfg instanceof CallEndNode); + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/CProjNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/CProjNode.java new file mode 100644 index 00000000..690ef590 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/CProjNode.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeTuple; +import java.util.BitSet; + +public class CProjNode extends CFGNode { + + // Which slice of the incoming multipart value + public final int _idx; + + // Debugging label + public final String _label; + + public CProjNode(Node ctrl, int idx, String label) { + super(ctrl); + _idx = idx; + _label = label; + } + public CProjNode(CProjNode c) { super(c); _idx = c._idx; _label = c._label; } + + @Override public String label() { return _label; } + + @Override StringBuilder _print1(StringBuilder sb, BitSet visited) { return sb.append(_label); } + + @Override public boolean isMultiTail() { return in(0).isMultiHead(); } + @Override public boolean blockHead() { return true; } + + public CFGNode ctrl() { return cfg(0); } + + @Override + public Type compute() { + Type t = ctrl()._type; + return t instanceof TypeTuple tt ? tt._types[_idx] : Type.BOTTOM; + } + + @Override + public Node idealize() { + if( ctrl()._type instanceof TypeTuple tt ) { + if( tt._types[_idx]==Type.XCONTROL ) + return Parser.XCTRL; // We are dead + if( ctrl() instanceof IfNode && tt._types[1-_idx]==Type.XCONTROL ) // Only true for IfNodes + return ctrl().in(0); // We become our input control + } + + // Flip a negating if-test, to remove the not + if( ctrl() instanceof IfNode iff && iff.pred().addDep(this) instanceof NotNode not ) + return new CProjNode(new IfNode(iff.ctrl(),not.in(1)).peephole(),1-_idx,_idx==0 ? "False" : "True"); + + // Copy of some other input + return ((MultiNode)ctrl()).pcopy(_idx); + } + + @Override + boolean eq( Node n ) { return _idx == ((CProjNode)n)._idx; } + + @Override + int hash() { return _idx; } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/CallEndNode.java new file mode 100644 index 00000000..4e50c1fc --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -0,0 +1,90 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; + +/** + * CallEnd + */ +public class CallEndNode extends CFGNode implements MultiNode { + + // When set true, this Call/CallEnd/Fun/Return is being trivially inlined + private boolean _folding; + + public CallEndNode(CallNode call) { super(new Node[]{call}); } + public CallEndNode(CallEndNode cend) { super(cend); } + + @Override + public String label() { return "CallEnd"; } + @Override public boolean isMultiHead() { return true; } + @Override public boolean blockHead() { return true; } + + CallNode call() { return (CallNode)in(0); } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("cend( "); + sb.append( in(0) instanceof CallNode ? "Call, " : "----, "); + for( int i=1; i<nIns()-1; i++ ) + in(i)._print0(sb,visited).append(","); + sb.setLength(sb.length()-1); + return sb.append(")"); + } + + @Override + public Type compute() { + if( !(in(0) instanceof CallNode call) ) + return TypeTuple.RET.dual(); + Type ret = call.fptr().addDep(this)._type instanceof TypeFunPtr tfp ? tfp.ret() : Type.BOTTOM; + return TypeTuple.make(call._type,TypeMem.BOT,ret); + } + + @Override + public Node idealize() { + + // Trivial inlining: call site calls a single function; single function + // is only called by this call site. + if( !_folding && nIns()==2 && in(0) instanceof CallNode call ) { + Node fptr = call.fptr(); + if( fptr.nOuts() == 1 && // Only user is this call + fptr instanceof ConstantNode && // We have an immediate call + // Function is being called, and its not-null + fptr._type instanceof TypeFunPtr tfp && tfp.notNull() && + // Arguments are correct + call.err()==null ) { + ReturnNode ret = (ReturnNode)in(1); + FunNode fun = ret.fun(); + // Expecting Start, and the Call + if( fun.nIns()==3 ) { + assert fun.in(1) instanceof StartNode && fun.in(2)==call; + // Disallow self-recursive inlining (loop unrolling by another name) + CFGNode idom = call; + while( !(idom instanceof FunNode fun2) ) + idom = idom.idom(); + if( idom != fun ) { + // Trivial inline: rewrite + _folding = true; + // Rewrite Fun so the normal RegionNode ideal collapses + fun._folding = true; + fun.setDef(1,Parser.XCTRL); // No default/unknown StartNode caller + fun.setDef(2,call.ctrl()); // Bypass the Call; + fun.ret().setDef(3,null); // Return is folding also + CodeGen.CODE.addAll(fun._outputs); + return this; + } + } else { + fun.addDep(this); + } + } else { // Function ptr has multiple users (so maybe multiple call sites) + fptr.addDep(this); + } + } + + return null; + } + + @Override public Node pcopy(int idx) { + return _folding ? in(1).in(idx) : null; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/CallNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/CallNode.java new file mode 100644 index 00000000..a9ca852c --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/CallNode.java @@ -0,0 +1,146 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; +import java.util.BitSet; + +/** + * Call + */ +public class CallNode extends CFGNode { + + // Source location for late reported errors + public final Parser.Lexer _loc; + + public CallNode(Parser.Lexer loc, Node... nodes) { super(nodes); _loc = loc; } + public CallNode(CallNode call) { super(call); _loc = call._loc; } + + @Override + public String label() { return "Call"; } + + @Override StringBuilder _print1(StringBuilder sb, BitSet visited) { + String fname = name(); + if( fname == null ) fptr()._print0(sb,visited); + else sb.append(fname); + sb.append("( "); + for( int i=2; i<nIns()-1; i++ ) + in(i)._print0(sb,visited).append(","); + sb.setLength(sb.length()-1); + return sb.append(")"); + } + public String name() { + if( fptr()._type instanceof TypeFunPtr tfp && tfp.isConstant() ) + return CodeGen.CODE.link(tfp)._name; + return null; + } + + + + Node ctrl() { return in(0); } + Node mem () { return in(1); } + // Args mapped 1-to-1 on inputs, so conceptually start at 2 + Node arg(int idx) { return in(idx); } + // Same arg accounting as TFPs, although the numbering starts at 2 + int nargs() { return nIns()-3; } // Minus control, memory, fptr + // args from input 2 to last; last is function input + public Node fptr() { return _inputs.last(); } + + // Find the Call End from the Call + public CallEndNode cend() { + // Always in use slot 0 + if( nOuts()>0 && out(0) instanceof CallEndNode cend ) { + assert _cend()==cend; + return cend; + } else { + assert _cend()==null; + return null; + } + } + private CallEndNode _cend() { + CallEndNode cend=null; + for( Node n : _outputs ) + if( n instanceof CallEndNode cend0 ) + { assert cend == null; cend = cend0; } + return cend; + } + + // Get the one control following; error to call with more than one e.g. an + // IfNode or other multi-way branch. + @Override public CFGNode uctrl() { return cend(); } + + + @Override + public Type compute() { + return ctrl()._type; + } + + @Override + public Node idealize() { + CallEndNode cend = cend(); + if( cend==null ) return null; // Still building + + // Link: call calls target function. Linking makes the target FunNode + // point to this Call, and all his Parms point to the call arguments; + // also the CallEnd points to the Return. + if( fptr()._type instanceof TypeFunPtr tfp && tfp.nargs() == nargs() ) { + // If fidxs is negative, then infinite unknown functions + long fidxs = tfp.fidxs(); + if( fidxs > 0 ) { + // Wipe out the return which matching in the linker table + // Walk the (63 max) bits and link + for( ; fidxs!=0; fidxs = TypeFunPtr.nextFIDX(fidxs) ) { + int fidx = Long.numberOfTrailingZeros(fidxs); + TypeFunPtr tfp0 = tfp.makeFrom(fidx); + FunNode fun = CodeGen.CODE.link(tfp0); + if( fun!=null && !fun._folding && !linked(fun) ) + link(fun); + } + } + } + + return null; + } + + // True if Fun is linked to this Call + boolean linked( FunNode fun ) { + for( Node n : fun._inputs ) + if( n == this ) + return true; + return false; + } + + + // Link so this calls fun + private void link( FunNode fun ) { + assert !linked(fun); + fun.addDef(this); + for( Node use : fun._outputs ) + if( use instanceof ParmNode parm ) + parm.addDef(parm._idx==0 ? cend() : arg(parm._idx)); + // Call end points to function return + CodeGen.CODE.add(cend()).addDef(fun.ret()); + assert linked(fun); + } + + @Override + public Parser.ParseException err() { + if( !(fptr()._type instanceof TypeFunPtr tfp) ) + throw Utils.TODO(); + if( !tfp.notNull() ) + return Parser.error( "Might be null calling "+tfp, _loc); + if( nargs() != tfp.nargs() ) + return Parser.error( "Expecting "+tfp.nargs()+" arguments, but found "+nargs(), _loc); + + // Check for args + for( int i=0; i<tfp.nargs(); i++ ) + if( !arg(i+2)._type.isa(tfp.arg(i)) ) + return Parser.error( "Argument #"+i+" isa "+arg(i+2)._type+", but must be a "+tfp.arg(i), _loc); + + return null; + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/CastNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/CastNode.java new file mode 100644 index 00000000..d8a968c1 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/CastNode.java @@ -0,0 +1,58 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; + +// Upcast (join) the input to a t. Used after guard test to lift an input. +// Can also be used to make a type-assertion if ctrl is null. +public class CastNode extends Node { + private final Type _t; + public CastNode(Type t, Node ctrl, Node in) { + super(ctrl, in); + _t = t; + setType(compute()); + } + + @Override public String label() { return "("+_t.str()+")"; } + + @Override + public String uniqueName() { return "Cast_" + _nid; } + + @Override public boolean isPinned() { return true; } + @Override public boolean isConst() { return true; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return in(1)._print0(sb.append(label()), visited); + } + + @Override + public Type compute() { + return in(1)._type.join(_t); + } + + @Override + public Node idealize() { + return in(1)._type.isa(_t) ? in(1) : null; + } + + @Override + boolean eq(Node n) { + CastNode cast = (CastNode)n; // Contract + return _t==cast._t; + } + + @Override + int hash() { return _t.hashCode(); } + + @Override + public Parser.ParseException err() { + // Has a condition to test, so OK + if( in(0) != null ) return null; + // No condition to test, so this must optimize away + throw Utils.TODO(); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/ConstantNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/ConstantNode.java new file mode 100644 index 00000000..d788cff7 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/ConstantNode.java @@ -0,0 +1,70 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; +import java.util.BitSet; + +/** + * A Constant node represents a constant value. At present, the only constants + * that we allow are integer literals; therefore Constants contain an integer + * value. As we add other types of constants, we will refactor how we represent + * Constants. + * <p> + * Constants have no semantic inputs. However, we set Start as an input to + * Constants to enable a forward graph walk. This edge carries no semantic + * meaning, and it is present <em>solely</em> to allow visitation. + * <p> + * The Constant's value is the value stored in it. + */ + +public class ConstantNode extends Node { + public final Type _con; + public ConstantNode( Type type ) { + super(new Node[]{CodeGen.CODE._start}); + _con = _type = type; + } + public ConstantNode( ConstantNode con ) { super(con); _con = con._type; } + + public static Node make( Type type ) { + if( type==Type. CONTROL ) return new CtrlNode(); + if( type==Type.XCONTROL ) return new XCtrlNode(); + return new ConstantNode(type); + } + + @Override public String label() { return "#"+_con; } + @Override public String glabel() { return _con.gprint(new SB().p("#")).toString(); } + + @Override + public String uniqueName() { return "Con_" + _nid; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + if( _con instanceof TypeFunPtr tfp ) { + FunNode fun = CodeGen.CODE.link(tfp); + if( fun._name != null ) + return sb.append("{ ").append(fun._name).append("}"); + } + return sb.append(_con.print(new SB())); + } + + @Override public boolean isMultiTail() { return true; } + @Override public boolean isConst() { return true; } + + @Override + public Type compute() { return _con; } + + @Override + public Node idealize() { return null; } + + @Override + boolean eq(Node n) { + ConstantNode con = (ConstantNode)n; // Contract + return _con==con._con; + } + + @Override + int hash() { return _con.hashCode(); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/CtrlNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/CtrlNode.java new file mode 100644 index 00000000..0ea060bd --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/CtrlNode.java @@ -0,0 +1,15 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import java.util.BitSet; + +public class CtrlNode extends CFGNode { + public CtrlNode() { super(CodeGen.CODE._start); } + @Override public String label() { return "Ctrl"; } + @Override StringBuilder _print1(StringBuilder sb, BitSet visited) { return sb.append("Cctrl"); } + @Override public boolean isConst() { return true; } + @Override public Type compute() { return Type.CONTROL; } + @Override public Node idealize() { return null; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/DivFNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/DivFNode.java new file mode 100644 index 00000000..36fd4524 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/DivFNode.java @@ -0,0 +1,46 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; + +import java.util.BitSet; + +public class DivFNode extends Node { + public DivFNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "DivF"; } + + @Override public String glabel() { return "/"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("/"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + if (in(1)._type instanceof TypeFloat i0 && + in(2)._type instanceof TypeFloat i1) { + if (i0.isConstant() && i1.isConstant()) + return TypeFloat.constant(i0.value()/i1.value()); + } + return in(1)._type.meet(in(2)._type); + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t1 = lhs._type; + Type t2 = rhs._type; + + // Div of 1. + if ( t2.isConstant() && t2 instanceof TypeFloat i && i.value()==1 ) + return lhs; + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new DivFNode(lhs,rhs); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/DivNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/DivNode.java new file mode 100644 index 00000000..0d5ed297 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/DivNode.java @@ -0,0 +1,46 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; + +public class DivNode extends Node { + public DivNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "Div"; } + + @Override public String glabel() { return "//"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("/"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1.isHigh() || t2.isHigh() ) + return TypeInteger.TOP; + if( t1 instanceof TypeInteger i1 && + t2 instanceof TypeInteger i2 ) { + if (i1.isConstant() && i2.isConstant()) + return i2.value() == 0 + ? TypeInteger.ZERO + : TypeInteger.constant(i1.value()/i2.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + // Div of 1. + if( in(2)._type == TypeInteger.TRUE ) + return in(1); + return null; + } + + @Override Node copy(Node lhs, Node rhs) { return new DivNode(lhs,rhs); } + @Override Node copyF() { return new DivFNode(null,null); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/FRefNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/FRefNode.java new file mode 100644 index 00000000..f0c4045d --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/FRefNode.java @@ -0,0 +1,35 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Var; +import com.seaofnodes.simple.type.Type; +import java.util.BitSet; + +/** + * A Forward Reference. Its any final constant, including functions. When + * the Def finally appears its plugged into the forward reference, which then + * peepholes to the Def. + */ +public class FRefNode extends ConstantNode { + public static final Type FREF_TYPE = Type.BOTTOM; + public final Var _n; + public FRefNode( Var n ) { super(FREF_TYPE); _n = n; } + + @Override public String label() { return "FRef"+_n; } + + @Override public String uniqueName() { return "FRef_" + _nid; } + + @Override StringBuilder _print1(StringBuilder sb, BitSet visited) { + return sb.append("FRef_").append(_n); + } + + @Override public Node idealize() { + // When FRef finds its definition, idealize to it + return nIns()==1 ? null : in(1); + } + + public Parser.ParseException err() { return Parser.error("Undefined name '"+_n._name+"'",_n._loc); } + + @Override boolean eq(Node n) { return this==n; } + @Override int hash() { return _n._idx; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/FunNode.java new file mode 100644 index 00000000..506e7758 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -0,0 +1,149 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; +import java.util.BitSet; +import static com.seaofnodes.simple.CodeGen.CODE; + +public class FunNode extends RegionNode { + + // When set true, this Call/CallEnd/Fun/Return is being trivially inlined + boolean _folding; + + private TypeFunPtr _sig; // Initial signature + private ReturnNode _ret; // Return pointer + + public String _name; // Debug name + + public FunNode( Parser.Lexer loc, TypeFunPtr sig, Node... nodes ) { super(loc,nodes); _sig = sig; } + public FunNode( FunNode fun ) { super( fun, fun._loc ); _sig = fun.sig(); _name = fun._name; } + + @Override + public String label() { return _name == null ? "$fun" : _name; } + + // Find the one CFG user from Fun. It's not always the Return, but always + // the Return *is* a CFG user of Fun. + @Override public CFGNode uctrl() { + for( Node n : _outputs ) + if( n instanceof CFGNode cfg && + (cfg instanceof RegionNode || cfg.cfg0()==this) ) + return cfg; + return null; + } + + public ParmNode rpc() { + ParmNode rpc = null; + for( Node n : _outputs ) + if( n instanceof ParmNode parm && parm._idx==0 ) + { assert rpc==null; rpc=parm; } + return rpc; + } + + // Cannot create the Return and Fun at the same time; one has to be first. + // So setting the return requires a second step. + public void setRet(ReturnNode ret) { _ret=ret; } + ReturnNode ret() { assert _ret!=null; return _ret; } + + // Signature can improve over time + public TypeFunPtr sig() { return _sig; } + void setSig( TypeFunPtr sig ) { + assert sig.isa(_sig); + if( _sig != sig ) { + CODE.add(this); + _sig = sig; + } + } + + @Override + public Type compute() { + // Only dead if no callers after SCCP + return Type.CONTROL; + } + + @Override + public Node idealize() { + + // Some linked path dies + Node progress = deadPath(); + if( progress!=null ) { + if( nIns()==3 && in(2) instanceof CallNode call ) + CODE.add(call.cend()); // If Start and one call, check for inline + return progress; + } + + // When can we assume no callers? Or no other callers (except main)? + // In a partial compilation, we assume Start gets access to any/all + // top-level public structures and recursively what they point to. + // This in turn is valid arguments to every callable function. + // + // In a total compilation, we can start from Start and keep things + // more contained. + + // If no default/unknown caller, use the normal RegionNode ideal rules + // to collapse + if( unknownCallers() ) return null; + + // If down to a single input, become that input + if( nIns()==2 && !hasPhi() ) { + CODE.add( CODE._stop ); // Stop will remove dead path + CODE.add( _ret ); // Return will compute to TOP control + return in(1); // Collapse if no Phis; 1-input Phis will collapse on their own + } + + return null; + } + + // Bypass Region idom, always assume depth == 1, one more than Start + @Override public int idepth() { return (_idepth=1); } + // Bypass Region idom, always assume idom is Start + @Override public CFGNode idom(Node dep) { return cfg(1); } + + // Always in-progress until we run out of unknown callers + public boolean unknownCallers() { return in(1) instanceof StartNode; } + + @Override public boolean inProgress() { return unknownCallers(); } + + // Add a new function exit point. + public void addReturn(Node ctrl, Node mem, Node rez) { _ret.addReturn(ctrl,mem,rez); } + + // Build the function body + public BitSet body() { + + // Reverse up (stop to start) CFG only, collect bitmap. + BitSet cfgs = new BitSet(); + cfgs.set(_nid); + walkUp(ret(),cfgs ); + + // Top down (start to stop) all flavors. CFG limit to bitmap. + // If data use bottoms out in wrong CFG, returns false - but tries all outputs. + // If any output hits an in-CFG use (e.g. phi), then keep node. + BitSet body = new BitSet(); + walkDown(this, cfgs, body, new BitSet()); + return body; + } + + private static void walkUp(CFGNode n, BitSet cfgs) { + if( cfgs.get(n._nid) ) return; + cfgs.set(n._nid); + if( n instanceof RegionNode r ) + for( int i=1; i<n.nIns(); i++ ) + walkUp(n.cfg(i),cfgs); + else walkUp(n.cfg0(),cfgs); + } + + private static boolean walkDown( Node n, BitSet cfgs, BitSet body, BitSet visit ) { + if( visit.get(n._nid) ) return body.get(n._nid); + visit.set(n._nid); + + if( n instanceof CFGNode && !cfgs.get(n._nid) ) + return false; + if( n.in(0)!=null && !cfgs.get(n.in(0)._nid) ) + return false; + boolean in = n.in(0)!=null || n instanceof CFGNode; + for( Node use : n._outputs ) + in |= walkDown(use,cfgs,body,visit); + if( in ) body.set(n._nid); + return in; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/IfNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/IfNode.java new file mode 100644 index 00000000..9e6d2dff --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/IfNode.java @@ -0,0 +1,69 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; +import java.util.HashSet; + +public class IfNode extends CFGNode implements MultiNode { + + public IfNode(Node ctrl, Node pred) { + super(ctrl, pred); + CodeGen.CODE.add(this); // Because idoms are complex, just add it + } + public IfNode(IfNode iff) { super(iff); } + + @Override + public String label() { return "If"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("if( "); + return in(1)._print0(sb, visited).append(" )"); + } + + @Override public boolean isMultiHead() { return true; } + + public Node ctrl() { return in(0); } + public Node pred() { return in(1); } + + // No one unique control follows + @Override public CFGNode uctrl() { return null; } + + @Override + public Type compute() { + // If the If node is not reachable then neither is any following Proj + if (ctrl()._type != Type.CONTROL && ctrl()._type != Type.BOTTOM ) + return TypeTuple.IF_NEITHER; + Node pred = pred(); + Type t = pred._type; + // High types mean NEITHER side is reachable. + // Wait until the type falls to decide which way to go. + if( t.isHigh() ) + return TypeTuple.IF_NEITHER; + // If constant is 0 then false branch is reachable + // Else true branch is reachable + if( t.isConstant() ) + return (t==Type.NIL || t==TypeInteger.ZERO || (t instanceof TypeFunPtr tfp && tfp._fidxs==0) ) ? TypeTuple.IF_FALSE : TypeTuple.IF_TRUE; + // If adding a zero makes a difference, the predicate must not have a zero/null + if( !t.makeZero().isa(t) ) + return TypeTuple.IF_TRUE; + + return TypeTuple.IF_BOTH; + } + + @Override + public Node idealize() { + // Hunt up the immediate dominator tree. If we find an identical if + // test on either the true or false branch, that side wins. + if( !pred()._type.isHighOrConst() ) + for( CFGNode dom = idom(), prior=this; dom!=null; prior = dom, dom = dom.idom() ) + if( dom.addDep(this) instanceof IfNode iff && iff.pred().addDep(this)==pred() && prior instanceof CProjNode prj ) { + setDef(1,con( prj._idx==0 ? 1 : 0 )); + return this; + } + return null; + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/LoadNode.java new file mode 100644 index 00000000..6c74042b --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/LoadNode.java @@ -0,0 +1,210 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; + +/** + * Load represents extracting a value from inside a memory object, + * in chapter 10 this means Struct fields. + */ +public class LoadNode extends MemOpNode { + + /** + * Load a value from a ptr.field. + * + * @param name The field we are loading + * @param mem The memory alias node - this is updated after a Store + * @param ptr The ptr to the struct base from where we load a field + * @param off The offset inside the struct base + */ + public LoadNode(Parser.Lexer loc, String name, int alias, Type glb, Node mem, Node ptr, Node off) { + super(loc, name, alias, glb, mem, ptr, off); + } + + // GraphVis DOT code (must be valid Java identifiers) and debugger labels + @Override public String label() { return "ld_"+mlabel(); } + // GraphVis node-internal labels + @Override public String glabel() { return "." +_name; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { return sb.append(".").append(_name); } + + @Override + public Type compute() { + if( mem()._type instanceof TypeMem mem ) { + // Update declared forward ref to the actual + if( _declaredType.isFRef() && mem._t instanceof TypeMemPtr tmp && !tmp.isFRef() ) + _declaredType = tmp; + // No lifting if ptr might null-check + if( err()==null ) + return _declaredType.join(mem._t); + } + return _declaredType; + } + + @Override + public Node idealize() { + Node ptr = ptr(); + Node mem = mem(); + + // Simple Load-after-Store on same address. + if( mem instanceof StoreNode st && + ptr == st.ptr() && off() == st.off() ) { // Must check same object + assert _name.equals(st._name); // Equiv class aliasing is perfect + return extend(st.val()); + } + + // Simple Load-after-New on same address. + if( mem instanceof ProjNode p && p.in(0) instanceof NewNode nnn && + ptr == nnn.proj(1) ) // Must check same object + return zero(nnn); // Load zero from new + + // Uplift control to a prior dominating load. + for( Node memuse : mem._outputs ) + // Find a prior load, has same mem,ptr,off but higher ctrl + if( memuse != this && memuse instanceof LoadNode ld && ptr==ld.ptr() && off()==ld.off() && + cfg0()!=null && cfg0()._idom(ld.cfg0(),this) == ld.cfg0() ) // Higher control means load is legal earlier + return ld; + + // Load-after-Store on same address, but bypassing provably unrelated + // stores. This is a more complex superset of the above two peeps. + // "Provably unrelated" is really weak. + if( ptr instanceof ReadOnlyNode ro ) + ptr = ro.in(1); + outer: + while( true ) { + switch( mem ) { + case StoreNode st: + if( ptr == st.ptr().addDep(this) && off() == st.off() ) + return extend(castRO(st.val())); // Proved equal + // Can we prove unequal? Offsets do not overlap? + if( !off()._type.join(st.off()._type).isHigh() && // Offsets overlap + !neverAlias(ptr,st.ptr()) ) // And might alias + break outer; // Cannot tell, stop trying + // Pointers cannot overlap + mem = st.mem(); // Proved never equal + break; + case PhiNode phi: + // Assume related + phi.addDep(this); + break outer; + case ConstantNode top: break outer; // Assume shortly dead + case ProjNode mproj: // Memory projection + switch( mproj.in(0) ) { + case NewNode nnn1: + if( ptr instanceof ProjNode pproj && pproj.in(0) == mproj.in(0) ) + return zero(nnn1); + if( !(ptr instanceof ProjNode pproj && pproj.in(0) instanceof NewNode) ) + break outer; // Cannot tell, ptr not related to New + mem = nnn1.in(nnn1.findAlias(_alias));// Bypass unrelated New + break; + case StartNode start: break outer; + case CallEndNode cend: break outer; // TODO: Bypass no-alias call + default: throw Utils.TODO(); + } + break; + case MemMergeNode merge: mem = merge.alias(_alias); break; + + default: + throw Utils.TODO(); + } + } + + // Push a Load up through a Phi, as long as it collapses on at least + // one arm. If at a Loop, the backedge MUST collapse - else we risk + // spinning the same transform around the loop indefinitely. + // BEFORE (2 Sts, 1 Ld): AFTER (1 St, 0 Ld): + // if( pred ) ptr.x = e0; val = pred ? e0 + // else ptr.x = e1; : e1; + // val = ptr.x; ptr.x = val; + if( mem() instanceof PhiNode memphi && memphi.region()._type == Type.CONTROL && memphi.nIns()== 3 && + // Offset can be hoisted + off() instanceof ConstantNode && + // Pointer can be hoisted + hoistPtr(ptr,memphi) ) { + + // Profit on RHS/Loop backedge + if( profit(memphi,2) || + // Else must not be a loop to count profit on LHS. + (!(memphi.region() instanceof LoopNode) && profit(memphi,1)) ) { + Node ld1 = ld(1); + Node ld2 = ld(2); + return new PhiNode(_name,_declaredType,memphi.region(),ld1,ld2); + } + } + + return null; + } + + // Load a flavored zero from a New + private Node zero(NewNode nnn) { + TypeStruct ts = nnn._ptr._obj; + Type zero = ts._fields[ts.findAlias(_alias)]._type.makeZero(); + return castRO(new ConstantNode(zero).peephole()); + } + + private Node ld( int idx ) { + Node mem = mem(), ptr = ptr(); + return new LoadNode(_loc,_name,_alias,_declaredType,mem.in(idx),ptr instanceof PhiNode && ptr.in(0)==mem.in(0) ? ptr.in(idx) : ptr, off()).peephole(); + } + + private static boolean neverAlias( Node ptr1, Node ptr2 ) { + return ptr1.in(0) != ptr2.in(0) && + // Unrelated allocations + ptr1 instanceof ProjNode && ptr1.in(0) instanceof NewNode && + ptr2 instanceof ProjNode && ptr2.in(0) instanceof NewNode; + } + + private static boolean hoistPtr(Node ptr, PhiNode memphi ) { + // Can I hoist ptr above the Region? + if( !(memphi.region() instanceof RegionNode r) ) + return false; // Dead or dying Region/Phi + // If ptr from same Region, then yes, just use hoisted split pointers + if( ptr instanceof PhiNode pphi && pphi.region() == r ) + return true; + + // No, so can we lift this ptr? + CFGNode cptr = ptr.cfg0(); + if( cptr != null ) + // Pointer is controlled high + // TODO: Really needs to be the LCA of all inputs is high + return cptr.idepth() <= r.idepth(); + + // Dunno without a longer walk + return false; + } + + // Profitable if we find a matching Store on this Phi arm. + private boolean profit(PhiNode phi, int idx) { + Node px = phi.in(idx); + if( px==null ) return false; + if( px._type instanceof TypeMem mem && mem._t.isHighOrConst() ) return true; + if( px instanceof StoreNode st1 && ptr()==st1.ptr() && off()==st1.off() ) return true; + px.addDep(this); + return false; + } + + // Read-Only is a deep property, and cannot be cast-away + private Node castRO(Node rez) { + if( ptr()._type.isFinal() && !rez._type.isFinal() ) + return new ReadOnlyNode(rez).peephole(); + return rez; + } + + // When a load bypasses a store, the store might truncate bits - and the + // load will need to zero/sign-extend. + private Node extend(Node val) { + if( !(_declaredType instanceof TypeInteger ti) ) return val; + if( ti._min==0 ) // Unsigned + return new AndNode(null,val,con(ti._max)); + // Signed extension + int shift = Long.numberOfLeadingZeros(ti._max)-1; + Node shf = con(shift); + if( shf._type==TypeInteger.ZERO ) + return val; + //return peep(new SarNode(null,peep(new ShlNode(null,val,shf.keep())),shf.unkeep())); + throw Utils.TODO(); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/LogicalNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/LogicalNode.java new file mode 100644 index 00000000..ccc1f6b5 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/LogicalNode.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; + +public abstract class LogicalNode extends Node { + // Source location for late reported errors + Parser.Lexer _loc; + + public LogicalNode(Parser.Lexer loc, Node lhs, Node rhs) { super(null, lhs, rhs); _loc = loc; } + abstract String op(); + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append(op()), visited); + return sb.append(")"); + } + + + @Override public Parser.ParseException err() { + if( in(1)._type.isHigh() || in(2)._type.isHigh() ) return null; + if( !(in(1)._type instanceof TypeInteger) ) return Parser.error("Cannot '"+op()+"' " + in(1)._type.glb(),_loc); + if( !(in(2)._type instanceof TypeInteger) ) return Parser.error("Cannot '"+op()+"' " + in(2)._type.glb(), _loc); + return null; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/LoopNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/LoopNode.java new file mode 100644 index 00000000..d2b0e36c --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/LoopNode.java @@ -0,0 +1,86 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; +import java.util.HashSet; + +public class LoopNode extends RegionNode { + public LoopNode( Parser.Lexer loc, Node entry ) { super(loc,null,entry,null); } + public LoopNode( LoopNode loop ) { super(loop); } + + public CFGNode entry() { return cfg(1); } + public CFGNode back () { return cfg(2); } + + @Override + public String label() { return "Loop"; } + + @Override + public Type compute() { + if( inProgress() ) return Type.CONTROL; + return entry()._type; + } + + // Bypass Region idom, same as the default idom() using use in(1) instead of in(0) + @Override public int idepth() { return _idepth==0 ? (_idepth=idom().idepth()+1) : _idepth; } + // Bypass Region idom, same as the default idom() using use in(1) instead of in(0) + @Override public CFGNode idom(Node dep) { return entry(); } + + // If this is an unreachable loop, it may not have an exit. If it does not + // (i.e., infinite loop), force an exit to make it reachable. + public StopNode forceExit( FunNode fun, StopNode stop ) { + // Walk the backedge, then immediate dominator tree util we hit this + // Loop again. If we ever hit a CProj from an If (as opposed to + // directly on the If) we found our exit. + CFGNode x = back(); + while( x != this ) { + if( x instanceof CProjNode exit && exit.in(0) instanceof IfNode iff ) { + CFGNode other = iff.cproj(1-exit._idx); + if( other!=null && other.loopDepth() < loopDepth() ) + return stop; // Found an exit, not an infinite loop + } + x = x.idom(); + } + // Found a no-exit loop. Insert an exit + NeverNode iff = new NeverNode(back()); + for( Node use : _outputs ) + if( use instanceof PhiNode ) + iff.addDef(use); + CProjNode t = new CProjNode(iff,0,"True" ).init(); + CProjNode f = new CProjNode(iff,1,"False").init(); + setDef(2,f); + + // Now fold control into the exit. Might have 1 valid exit, or an + // XCtrl or a bunch of prior NeverNode exits. + Node top = new ConstantNode(Type.TOP).peephole(); + ReturnNode ret = fun.ret(); + Node ctrl = ret.ctrl(), mem = ret.mem(), expr = ret.expr(); + if( ctrl._type != Type.XCONTROL ) { + // Perfect aligned exit? + if( !(ctrl instanceof RegionNode r && + mem instanceof PhiNode pmem && pmem.region()==r && + expr instanceof PhiNode prez && prez.region()==r ) ) { + // Nope, insert an aligned exit layer + ctrl = new RegionNode(_loc,null,ctrl).init(); + mem = new PhiNode((RegionNode)ctrl,mem ).init(); + expr = new PhiNode((RegionNode)ctrl,expr).init(); + } + // Append new Never exit + ctrl.addDef(t ); + mem .addDef(top); + expr.addDef(top); + } else { + ctrl = t; + mem = top; + expr = top; + } + ret.setDef(0,ctrl); + ret.setDef(1,mem ); + ret.setDef(2,expr); + + return stop; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/MachConcreteNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/MachConcreteNode.java new file mode 100644 index 00000000..0c49fc64 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/MachConcreteNode.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.Utils; +import java.io.ByteArrayOutputStream; +import java.util.BitSet; + +// Generic machine-specific class, has a few Node implementations that have to +// exist (abstract) but are not useful past the optimizer. +public abstract class MachConcreteNode extends Node implements MachNode{ + + public MachConcreteNode(Node node) { super(node); } + public MachConcreteNode(Node[]nodes) { super(nodes); } + + @Override public String label() { return op(); } + @Override public Type compute () { throw Utils.TODO(); } + @Override public Node idealize() { throw Utils.TODO(); } + + @Override public StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("(").append(op()).append(","); + for( int i=1; i<nIns(); i++ ) + (in(i)==null ? sb.append(" ---") : in(i)._print0(sb, visited)).append(","); + sb.setLength(sb.length()-1); + return sb.append(")"); + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/MachNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/MachNode.java new file mode 100644 index 00000000..9f2fee85 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/MachNode.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.*; +import java.io.ByteArrayOutputStream; + +public interface MachNode { + + // Easy access to the abstract Machine + default Machine machine() { return CodeGen.CODE._mach; } + + // Run a post-instruction-selection action + default void postSelect() { } + + // Register mask allowed on input i. 0 for no register. + abstract public RegMask regmap(int i); + // Register mask allowed as a result. 0 for no register. + abstract public RegMask outregmap(); + // Multi-reg-defining machine op; idx comes from Proj. + // Sometimes these Projs have no uses, and just exist to kill a register. + default RegMask outregmap(int idx) { throw Utils.TODO(); } + + // If this op updates the same register as input 'idx', e.g. many X86 "R1 += + // R2" style ops. This forces the output live range to equal the input live + // range, so they both get the same register. + + // This can hold true for projections, if the instruction updates many + // registers that it also takes in. Note that ops also use the same + // registers for both inputs and outputs but these are pinned and without any + // choices (e.g. always RCX for both an input and output) and are better + // served with just declaring as a fixed register. + + // Returns either 0 for not-two-adr or the updated input; for projections + // this is their Multi's updated input. + default int twoAddress( ) { return 0; } + + // Instructions cheaper to recreate than to spill, such as loading small constants + default boolean isClone() { return false; } + // Encoding is appended into the byte array; size is returned + abstract public int encoding(ByteArrayOutputStream bytes); + + // Human-readable form appended to the SB. Things like the encoding, + // indentation, leading address or block labels not printed here. + // Just something like "R17=[R18+12] // Load array base". + // General form: "dst=src+src". + default void asm(CodeGen code, SB sb) { throw Utils.TODO(); } + + // Human readable opcode; e.g. "ld4" or "call". + default String op() { throw Utils.TODO(); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java new file mode 100644 index 00000000..5363bb6f --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java @@ -0,0 +1,154 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.type.*; +import java.util.*; + +/** + * Memory Merge - a merge of many aliases into a "fat memory". All aliases + * are here, but most will be lazy - take the default fat memory. + */ +public class MemMergeNode extends Node { + + /* + * In-Progress means this is being used by the Parser to track memory + * aliases. No optimizations are allowed. When no longer "in progress" + * normal peeps work. + */ + public final boolean _inProgress; + + public MemMergeNode( boolean inProgress) { _type = TypeMem.BOT; _inProgress = inProgress; } + public MemMergeNode(MemMergeNode mem) { super(mem); _inProgress = false; } + + + // If being used by a Scope, this is "in progress" from the Parser. + // Otherwise, it's a memory merge + boolean inProgress() { return _inProgress; } + + @Override public String label() { return "ALLMEM"; } + @Override public boolean isMem() { return true; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append(_inProgress ? "Merge[" : "MEM[ "); + for( int j=2; j<nIns(); j++ ) { + sb.append(j); + sb.append(":"); + Node n = in(j); + while( n instanceof ScopeNode loop ) { + sb.append("Lazy_"); + n = loop.mem(j); + } + if( n==null ) sb.append("___ "); + else n._print0(sb, visited).append(" "); + } + sb.setLength(sb.length()-1); + return sb.append("]"); + } + + // Make a memory merge: no longer a Scope really, tracking memory state but + // not related to the parser in any way. + public Node merge() { + // Force default memory to not be lazy + MemMergeNode merge = new MemMergeNode(false); + for( Node n : _inputs ) + merge.addDef(n); + merge._mem(1,null); + return merge.peephole(); + } + + + @Override public Type compute() { return TypeMem.BOT; } + + @Override public Node idealize() { + if( inProgress() ) return null; + + // If not merging any memory (all memory is just the default) + if( nIns()==2 ) + return in(1); // Become default memory + + return null; + } + + public Node in( Var v ) { return in(v._idx); } + + public Node alias( int alias ) { + return in(alias<nIns() && in(alias)!=null ? alias : 1); + } + + Node alias( int alias, Node st ) { + while( alias >= nIns() ) addDef(null); + return setDef(alias,st); + } + + // Read or update from memory. + // A shared implementation allows us to create lazy phis both during + // lookups and updates; the lazy phi creation is part of chapter 8. + Node _mem( int alias, Node st ) { + // Memory projections are made lazily; if one does not exist + // then it must be START.proj(1) + Node old = alias(alias); + if( old instanceof ScopeNode loop ) { + MemMergeNode loopmem = loop.mem(); + Node memdef = loopmem.alias(alias); + // Lazy phi! + old = memdef instanceof PhiNode phi && loop.ctrl()==phi.region() + // Loop already has a real Phi, use it + ? memdef + // Set real Phi in the loop head + // The phi takes its one input (no backedge yet) from a recursive + // lookup, which might have insert a Phi in every loop nest. + : loopmem.alias(alias, new PhiNode(Parser.memName(alias), TypeMem.BOT,loop.ctrl(),loopmem._mem(alias,null),null).peephole() ); + alias(alias,old); + } + // Memory projections are made lazily; expand as needed + return st==null ? old : alias(alias,st); // Not lazy, so this is the answer + } + + + void _merge( MemMergeNode that, RegionNode r) { + int len = Math.max(nIns(),that.nIns()); + for( int i = 2; i < len; i++) + if( alias(i) != that.alias(i) ) { // No need for redundant Phis + // If we are in lazy phi mode we need to a lookup + // by alias as it will trigger a phi creation + Node lhs = this._mem(i,null); + Node rhs = that._mem(i,null); + alias(i, new PhiNode(Parser.memName(i), lhs._type.glb().meet(rhs._type.glb()), r, lhs, rhs).peephole()); + } + } + + // Fill in the backedge of any inserted Phis + void _endLoopMem( ScopeNode scope, MemMergeNode back, MemMergeNode exit ) { + Node exit_def = exit.alias(1); + for( int i=1; i<nIns(); i++ ) { + if( in(i) instanceof PhiNode phi && phi.region()==scope.ctrl() ) { + assert phi.in(2)==null; + phi.setDef(2,back.in(i)==scope ? phi : back.in(i)); // Fill backedge + } + if( exit_def == scope ) // Replace a lazy-phi on the exit path also + exit.alias(i,in(i)); + } + } + + // Now one-time do a useless-phi removal + void _useless( ) { + for( int i=2; i<nIns(); i++ ) { + if( in(i) instanceof PhiNode phi ) { + // Do an eager useless-phi removal + Node in = phi.peephole(); + CodeGen.CODE.addAll(phi._outputs); + phi.moveDepsToWorklist(); + if( in != phi ) { + if( !phi.iskeep() ) // Keeping phi around for parser elsewhere + phi.subsume(in); + setDef(i,in); // Set the update back into Scope + } + } + } + } + + @Override boolean eq( Node n ) { + return this==n || !_inProgress; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/MemOpNode.java new file mode 100644 index 00000000..d9ab6b89 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/MemOpNode.java @@ -0,0 +1,74 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMemPtr; +import java.lang.StringBuilder; +import java.util.BitSet; + +/** + * Convenience common base for Load and Store. + */ +public abstract class MemOpNode extends Node { + + public final String _name; + public final int _alias; + // Declared type; not final because it might be a forward-reference + // which will be lazily improved when the reference is declared. + public Type _declaredType; + // Source location for late reported errors + public final Parser.Lexer _loc; + + public MemOpNode(Parser.Lexer loc, String name, int alias, Type glb, Node mem, Node ptr, Node off) { + super(null, mem, ptr, off); + _name = name; + _alias = alias; + _declaredType = glb; + _loc = loc; + } + public MemOpNode(Parser.Lexer loc, String name, int alias, Type glb, Node mem, Node ptr, Node off, Node value) { + this(loc,name, alias, glb, mem, ptr, off); + addDef(value); + } + public MemOpNode( Node mach, MemOpNode mop ) { + super(mach); + _name = mop._name; + _alias = mop._alias; + _declaredType = mop._declaredType; + _loc = mop._loc; + } + + // + static String mlabel(String name) { return name.equals("[]") ? "ary" : (name.equals("#") ? "len" : name); } + String mlabel() { return mlabel(_name); } + + public Node mem() { return in(1); } + public Node ptr() { return in(2); } + public Node off() { return in(3); } + + @Override StringBuilder _print1( StringBuilder sb, BitSet visited ) { return _printMach(sb,visited); } + public StringBuilder _printMach( StringBuilder sb, BitSet visited ) { throw Utils.TODO(); } + + + @Override + boolean eq(Node n) { + MemOpNode mem = (MemOpNode)n; // Invariant + return _alias==mem._alias; // When comparing types error to use "equals"; always use "==" + } + + @Override + int hash() { return _alias; } + + @Override + public Parser.ParseException err() { + Type ptr = ptr()._type; + // Already an error, but better error messages come from elsewhere + if( ptr == Type.BOTTOM ) return null; + if( ptr.isHigh() ) return null; // Assume it will fall to not-null + // Better be a not-nil TMP + if( ptr instanceof TypeMemPtr tmp && tmp.notNull() ) + return null; + return Parser.error( "Might be null accessing '" + _name + "'",_loc); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/MinusFNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/MinusFNode.java new file mode 100644 index 00000000..2b749ba3 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/MinusFNode.java @@ -0,0 +1,36 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; + +import java.util.BitSet; + +public class MinusFNode extends Node { + public MinusFNode(Node in) { super(null, in); } + + @Override public String label() { return "MinusF"; } + + @Override public String glabel() { return "-"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("(-"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + if (in(1)._type instanceof TypeFloat i0) + return i0.isConstant() ? TypeFloat.constant(-i0.value()) : i0; + return TypeFloat.F64; + } + + @Override + public Node idealize() { + // -(-x) is x + if( in(1) instanceof MinusFNode minus ) + return minus.in(1); + + return null; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/MinusNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/MinusNode.java new file mode 100644 index 00000000..dd2cbd26 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/MinusNode.java @@ -0,0 +1,40 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.Utils; + +import java.util.BitSet; + +public class MinusNode extends Node { + public MinusNode(Node in) { super(null, in); } + + @Override public String label() { return "Minus"; } + + @Override public String glabel() { return "-"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("(-"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + if( in(1)._type.isHigh() ) return TypeInteger.TOP; + if( in(1)._type instanceof TypeInteger i0 && + !(i0==TypeInteger.BOT || i0._min == Long.MIN_VALUE || i0._max == Long.MIN_VALUE) ) + return TypeInteger.make(-i0._max,-i0._min); + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + // -(-x) is x + if( in(1) instanceof MinusNode minus ) + return minus.in(1); + + return null; + } + @Override Node copyF() { return new MinusFNode(null); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/MulFNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/MulFNode.java new file mode 100644 index 00000000..2d44a9fb --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/MulFNode.java @@ -0,0 +1,51 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; + +import java.util.BitSet; + +public class MulFNode extends Node { + public MulFNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "MulF"; } + + @Override public String glabel() { return "*"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("*"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + if (in(1)._type instanceof TypeFloat i0 && + in(2)._type instanceof TypeFloat i1) { + if (i0.isConstant() && i1.isConstant()) + return TypeFloat.constant(i0.value()*i1.value()); + } + return in(1)._type.meet(in(2)._type); + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t1 = lhs._type; + Type t2 = rhs._type; + + // Mul of 1. We do not check for (1*x) because this will already + // canonicalize to (x*1) + if ( t2.isConstant() && t2 instanceof TypeFloat i && i.value()==1 ) + return lhs; + + // Move constants to RHS: con*arg becomes arg*con + if ( t1.isConstant() && !t2.isConstant() ) + return swap12(); + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new MulFNode(lhs,rhs); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/MulNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/MulNode.java new file mode 100644 index 00000000..4fdf987d --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/MulNode.java @@ -0,0 +1,81 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; + +public class MulNode extends Node { + public MulNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "Mul"; } + + @Override public String glabel() { return "*"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("*"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1.isHigh() || t2.isHigh() ) + return TypeInteger.TOP; + if( t1 instanceof TypeInteger i1 && + t2 instanceof TypeInteger i2 ) { + if( i1==TypeInteger.ZERO || i2==TypeInteger.ZERO) + return TypeInteger.ZERO; + if (i1.isConstant() && i2.isConstant()) + return TypeInteger.constant(i1.value()*i2.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t1 = lhs._type; + Type t2 = rhs._type; + + // Move constants to RHS: con*arg becomes arg*con + if ( t1.isConstant() && !t2.isConstant() ) + return swap12(); + + // Multiply by constant + if ( t2.isConstant() && t2 instanceof TypeInteger i ) { + // Mul of 1. We do not check for (1*x) because this will already + // canonicalize to (x*1) + long c = i.value(); + if( c==1 ) return lhs; + if( c==0 ) return Parser.ZERO; + // Mul by a power of 2, +/-1. Bit patterns more complex than this + // are unlikely to win on an X86 vs the normal "imul", and so + // become machine-specific. + if( (c & (c-1)) == 0 ) + return new ShlNode(null,lhs,con(Long.numberOfTrailingZeros(c))); + // 2**n + 1, e.g. 9 + long d = c-1; + if( (d & (d-1)) == 0 ) + return new AddNode(new ShlNode(null,lhs,con(Long.numberOfTrailingZeros(d))).peephole(),lhs); + // 2**n - 1, e.g. 31 + long e = c+1; + if( (e & (e-1)) == 0 ) + return new SubNode(new ShlNode(null,lhs,con(Long.numberOfTrailingZeros(e))).peephole(),lhs); + + } + + // Do we have ((x * (phi cons)) * con) ? + // Do we have ((x * (phi cons)) * (phi cons)) ? + // Push constant up through the phi: x * (phi con0*con0 con1*con1...) + Node phicon = AddNode.phiCon(this,true); + if( phicon!=null ) return phicon; + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new MulNode(lhs,rhs); } + @Override Node copyF() { return new MulFNode(null,null); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/MultiNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/MultiNode.java new file mode 100644 index 00000000..17d00881 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/MultiNode.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Ary; + + +public interface MultiNode { + + abstract Ary<Node> outs(); + + // Find a projection by index + default ProjNode proj( int idx ) { + for( Node out : outs() ) + if( out instanceof ProjNode prj && prj._idx==idx ) + return prj; + return null; + } + + // Find a projection by index + default CProjNode cproj( int idx ) { + for( Node out : outs() ) + if( out instanceof CProjNode prj && prj._idx==idx ) + return prj; + return null; + } + + // Return not-null if this projection index is a ideal copy. + // Called by ProjNode ideal and used to collapse Multis. + default Node pcopy(int idx) { return null; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/NeverNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/NeverNode.java new file mode 100644 index 00000000..e77ed653 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/NeverNode.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeTuple; + +import java.util.BitSet; + +// "Never true" for infinite loop exits +public class NeverNode extends IfNode { + public NeverNode(Node ctrl) { super(ctrl,Parser.ZERO); } + + @Override public String label() { return "Never"; } + + @Override StringBuilder _print1(StringBuilder sb, BitSet visited) { return sb.append("Never"); } + + @Override public Type compute() { return TypeTuple.IF_BOTH; } + + @Override public Node idealize() { return null; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/NewNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/NewNode.java new file mode 100644 index 00000000..80545d66 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/NewNode.java @@ -0,0 +1,83 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.Utils; +import java.util.BitSet; + +/** + * Allocation! Allocate a chunk of memory, and pre-zero it. + * The inputs include control and size, and ALL aliases being set. + * The output is large tuple, one for every alias plus the created pointer. + * New is expected to be followed by projections for every alias. + */ +public class NewNode extends Node implements MultiNode { + + public final TypeMemPtr _ptr; + public final int _len; + + public NewNode(TypeMemPtr ptr, Node... nodes) { + super(nodes); + assert !ptr.nullable(); + _ptr = ptr; + _len = ptr._obj._fields.length; + // Control in slot 0 + assert nodes[0]._type==Type.CONTROL || nodes[0]._type == Type.XCONTROL; + // Malloc-length in slot 1 + assert nodes[1]._type instanceof TypeInteger || nodes[1]._type==Type.NIL; + for( int i=0; i<_len; i++ ) + // Memory slices for all fields. + assert nodes[2+i]._type.isa(TypeMem.BOT); + } + + public NewNode(NewNode nnn) { super(nnn); _ptr = nnn._ptr; _len = nnn._len; } + + public Node mem (int idx) { return in(idx+2); } + + @Override public String label() { return "new_"+glabel(); } + @Override public String glabel() { + return _ptr._obj.isAry() ? "ary_"+_ptr._obj._fields[1]._type.str() : _ptr._obj.str(); + } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("new "); + return sb.append(_ptr._obj.str()); + } + + int findAlias(int alias) { + int idx = _ptr._obj.findAlias(alias); + assert idx!= -1; // Error, caller should be calling + return idx+2; // Skip ctrl, size + } + + // 0 - ctrl + // 1 - byte size + // 2-len+2 - aliases, one per field + // len+2 - 2*len+2 - initial values, one per field + public Node size() { return in(1); } + + @Override + public TypeTuple compute() { + Field[] fs = _ptr._obj._fields; + Type[] ts = new Type[fs.length+2]; + ts[0] = Type.CONTROL; + ts[1] = _ptr; + for( int i=0; i<fs.length; i++ ) { + Type mt = in(i+2)._type; + TypeMem mem = mt==Type.TOP ? TypeMem.TOP : (TypeMem)mt; + Type tfld = mem._t.meet(mem._t.makeZero()); + Type tfld2 = tfld.join(fs[i]._type); + ts[i+2] = TypeMem.make(fs[i]._alias,tfld2); + } + return TypeTuple.make(ts); + } + + @Override + public Node idealize() { return null; } + + @Override + boolean eq(Node n) { return this == n; } + + @Override + int hash() { return _ptr.hashCode(); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/Node.java b/chapter20/src/main/java/com/seaofnodes/simple/node/Node.java new file mode 100644 index 00000000..bbf1ebe9 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/Node.java @@ -0,0 +1,640 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.*; +import java.util.function.Function; +import static com.seaofnodes.simple.CodeGen.CODE; + +/** + * All Nodes in the Sea of Nodes IR inherit from the Node class. + * The Node class provides common functionality used by all subtypes. + * Subtypes of Node specialize by overriding methods. + */ +public abstract class Node { + + /** + * Each node has a unique dense Node ID within a compilation context + * The ID is useful for debugging, for using as an offset in a bitvector, + * as well as for computing equality of nodes (to be implemented later). + */ + public final int _nid; + + /** + * Inputs to the node. These are use-def references to Nodes. + * <p> + * Generally fixed length, ordered, nulls allowed, no unused trailing space. + * Ordering is required because e.g. "a/b" is different from "b/a". + * The first input (offset 0) is often a {@link CFGNode} node. + */ + public final Ary<Node> _inputs; + + /** + * Outputs reference Nodes that are not null and have this Node as an + * input. These nodes are users of this node, thus these are def-use + * references to Nodes. + * <p> + * Outputs directly match inputs, making a directed graph that can be + * walked in either direction. These outputs are typically used for + * efficient optimizations but otherwise have no semantics meaning. + */ + public final Ary<Node> _outputs; + + + /** + * Current computed type for this Node. This value changes as the graph + * changes and more knowledge is gained about the program. + */ + public Type _type; + + + Node(Node... inputs) { + _nid = CODE.getUID(); // allocate unique dense ID + _inputs = new Ary<>(Node.class); + Collections.addAll(_inputs,inputs); + _outputs = new Ary<>(Node.class); + for( Node n : _inputs ) + if( n != null ) + n.addUse( this ); + } + + // Make a Node using the existing arrays of nodes. + // Used by any pass rewriting all Node classes but not the edges. + Node( Node n ) { + assert CodeGen.CODE._phase == CodeGen.Phase.InstructionSelection; + _nid = CODE.getUID(); // allocate unique dense ID + _inputs = new Ary<>(n._inputs.asAry()); + _outputs = new Ary<>(Node.class); + _type = n._type; + _deps = null; + _hash = 0; + } + + // Easy reading label for debugger, e.g. "Add" or "Region" or "EQ" + public abstract String label(); + + // Unique label for graph visualization, e.g. "Add12" or "Region30" or "EQ99" + public String uniqueName() { + // Get rid of $ as graphviz doesn't like it + String label = label().replaceAll("\\$", ""); + return label + _nid; + } + + // Graphical label, e.g. "+" or "Region" or "==" + public String glabel() { return label(); } + + // Extra fun stuff, for assembly printing. Jump labels, parser locations, + // variable types, etc. + public String comment() { return null; } + + // ------------------------------------------------------------------------ + + // Debugger Printing. + + // {@code toString} is what you get in the debugger. It has to print 1 + // line (because this is what a debugger typically displays by default) and + // has to be robust with broken graph/nodes. + @Override + public final String toString() { return print(); } + + // This is a *deep* print. We print with a mutually recursive tik-tok + // style; the common _print0 calls the per-Node _print1, which calls back + // to _print0; + public final String print() { + return _print0(new StringBuilder(), new BitSet()).toString(); + } + + // This is the common print: check for repeats, check for DEAD and print + // "DEAD" else call the per-Node print1. + final StringBuilder _print0(StringBuilder sb, BitSet visited) { + if (visited.get(_nid) && !(this instanceof ConstantNode) ) + return sb.append(label()); + visited.set(_nid); + return isDead() + ? sb.append(uniqueName()).append(":DEAD") + : _print1(sb, visited); + } + // Every Node implements this; a partial-line recursive print + abstract StringBuilder _print1(StringBuilder sb, BitSet visited); + + public String p(int depth) { return IRPrinter.prettyPrint(this,depth); } + + public boolean isMultiHead() { return false; } + public boolean isMultiTail() { return false; } + public boolean isConst () { return false; } + + // ------------------------------------------------------------------------ + // Graph Node & Edge manipulation + + /** + * Gets the ith input node + * @param i Offset of the input node + * @return Input node or null + */ + public Node in(int i) { return _inputs.get(i); } + public Node out(int i) { return _outputs.get(i); } + public final Ary<Node> outs() { return _outputs; } + + public int nIns() { return _inputs.size(); } + + public int nOuts() { return _outputs.size(); } + + public boolean isUnused() { return nOuts() == 0; } + + public CFGNode cfg0() { return (CFGNode)in(0); } + + /** + * Change a <em>def</em> into a Node. Keeps the edges correct, by removing + * the corresponding <em>use->def</em> edge. This may make the original + * <em>def</em> go dead. This function is co-recursive with {@link #kill}. + * <p> + + * This method is the normal path for altering a Node, because it does the + * proper default edge maintenance. It also <em>immediately</em> kills + * Nodes that lose their last use; at times care must be taken to avoid + * killing Nodes that are being used without having an output Node. This + * definitely happens in the middle of recursive {@link #peephole} calls. + * + * @param idx which def to set + * @param new_def the new definition + * @return new_def for flow coding + */ + public <N extends Node> N setDef(int idx, N new_def ) { + unlock(); + Node old_def = in(idx); + if( old_def == new_def ) return new_def; // No change + // If new def is not null, add the corresponding def->use edge + // This needs to happen before removing the old node's def->use edge as + // the new_def might get killed if the old node kills it recursively. + if( new_def != null ) + new_def.addUse(this); + // Set the new_def over the old (killed) edge + _inputs.set(idx,new_def); + if( old_def != null ) { // If the old def exists, remove a def->use edge + if( old_def.delUse(this) ) // If we removed the last use, the old def is now dead + old_def.kill(); // Kill old def + else CODE.add(old_def); // Else old lost a use, so onto worklist + } + moveDepsToWorklist(); + // Return new_def for easy flow-coding + return new_def; + } + + // Remove the numbered input, compressing the inputs in-place. This + // shuffles the order deterministically - which is suitable for Region and + // Phi, but not for every Node. If the def goes dead, it is recursively + // killed, which may include 'this' Node. + Node delDef(int idx) { + unlock(); + Node old_def = in(idx); + _inputs.del(idx); + if( old_def.delUse(this) ) // If we removed the last use, the old def is now dead + old_def.kill(); // Kill old def + old_def.moveDepsToWorklist(); + return this; + } + + // Insert the numbered input, sliding other inputs to the right + Node insertDef(int idx, Node new_def) { + _inputs.add(idx,null); + return setDef(idx,new_def); + } + + + /** + * Add a new def to an existing Node. Keep the edges correct by + * adding the corresponding <em>def->use</em> edge. + * + * @param new_def the new definition, appended to the end of existing definitions + * @return new_def for flow coding + */ + public Node addDef(Node new_def) { + unlock(); + // Add use->def edge + _inputs.add(new_def); + // If new def is not null, add the corresponding def->use edge + if( new_def != null ) + new_def.addUse(this); + return new_def; + } + + // Breaks the edge invariants, used temporarily + protected <N extends Node> N addUse(Node n) { _outputs.add(n); return (N)this; } + + // Remove node 'use' from 'def's (i.e. our) output list, by compressing the list in-place. + // Return true if the output list is empty afterward. + // Error is 'use' does not exist; ok for 'use' to be null. + protected boolean delUse( Node use ) { + _outputs.del(_outputs.find(use)); + return _outputs.isEmpty(); + } + + // Shortcut for "popping" until n nodes. A "pop" is basically a + // setDef(last,null) followed by lowering the nIns() count. + void popUntil(int n) { + unlock(); + while( nIns() > n ) { + Node old_def = _inputs.pop(); + if( old_def != null && // If it exists and + old_def.delUse(this) ) // If we removed the last use, the old def is now dead + old_def.kill(); // Kill old def + } + } + + /** + * Kill a Node with no <em>uses</em>, by setting all of its <em>defs</em> + * to null. This may recursively kill more Nodes, and is basically dead + * code elimination. + */ + public void kill( ) { + unlock(); + moveDepsToWorklist(); + assert isUnused(); // Has no uses, so it is dead + _type=null; // Flag as dead + while( nIns()>0 ) { // Set all inputs to null, recursively killing unused Nodes + Node old_def = _inputs.removeLast(); + // Revisit neighbor because removed use + if( old_def != null && CODE.add(old_def).delUse(this) ) + old_def.kill(); // If we removed the last use, the old def is now dead + } + assert isDead(); // Really dead now + } + + // Mostly used for asserts and printing. + public boolean isDead() { return isUnused() && nIns()==0 && _type==null; } + + // Shortcuts to stop DCE mid-parse + // Add bogus null use to keep node alive + public <N extends Node> N keep() { return addUse(null); } + // Remove bogus null. + public <N extends Node> N unkeep() { + delUse(null); + return (N)this; + } + // Test "keep" status + public boolean iskeep() { return _outputs.find(null) != -1; } + public void unkill() { + if( unkeep().isUnused() ) + kill(); + } + + + // Replace self with nnn in the graph, making 'this' go dead + public void subsume( Node nnn ) { + assert nnn!=this; + while( nOuts() > 0 ) { + Node n = _outputs.removeLast(); + n.unlock(); + int idx = n._inputs.find(this); + n._inputs.set(idx,nnn); + nnn.addUse(n); + CODE.addAll(n._outputs); + } + kill(); + } + + // ------------------------------------------------------------------------ + // Graph-based optimizations + + /** + * Try to peephole at this node and return a better replacement Node. + * Always returns some not-null Node (often this). + */ + public final Node peephole( ) { + if( _type==null ) // Brand-new node, never peeped before + JSViewer.show(); + Node n = peepholeOpt(); + if( n!=null ) // Made progress? + JSViewer.show(); // Show again + return n==null ? this : deadCodeElim(n._nid >= _nid ? n.peephole() : n); // Cannot return null for no-progress + } + + /** + * Try to peephole at this node and return a better replacement Node if + * possible. We compute a {@link Type} and then check and replace: + * <ul> + * <li>if the Type {@link Type#isConstant}, we replace with a {@link ConstantNode}</li> + * <li>in a future chapter we will look for a + * <a href="https://en.wikipedia.org/wiki/Common_subexpression_elimination">Common Subexpression</a> + * to eliminate.</li> + * <li>we ask the Node for a better replacement. The "better replacement" + * is things like {@code (1+2)} becomes {@code 3} and {@code (1+(x+2))} becomes + * {@code (x+(1+2))}. By canonicalizing expressions we fold common addressing + * math constants, remove algebraic identities and generally simplify the + * code. </li> + * + * Unlike peephole above, this explicitly returns null for no-change, or not-null + * for a better replacement (which can be this). + * </ul> + */ + public final Node peepholeOpt( ) { + CODE.iterCnt(); + // Compute initial or improved Type + Type old = setType(compute()); + + // Replace constant computations from non-constants with a constant + // node. If peeps are disabled, still allow high Phis to collapse; + // they typically come from dead Regions, and we want the Region to + // collapse, which requires the Phis to die first. + if( _type.isHighOrConst() && !isConst() ) + return ConstantNode.make(_type).peephole(); + + // Global Value Numbering + if( _hash==0 ) { + Node n = CODE._gvn.get(this); // Will set _hash as a side effect + if( n==null ) + CODE._gvn.put(this,this); // Put in table now + else { + // Because of random worklist ordering, the two equal nodes + // might have different types. Because of monotonicity, both + // types are valid. To preserve monotonicity, the resulting + // shared Node has to have the best of both types. + n.setType(n._type.join(_type)); + _hash = 0; // Clear, since it never went in the table + return deadCodeElim(n);// Return previous; does Common Subexpression Elimination + } + } + + // Ask each node for a better replacement + Node n = idealize(); + if( n != null ) // Something changed + return n; // Report progress + + if( old!=_type ) return this; // Report progress; + CODE.iterNop(); + return null; // No progress + } + + // m is the new Node, self is the old. + // Return 'm', which may have zero uses but is alive nonetheless. + // If self has zero uses (and is not 'm'), {@link #kill} self. + private Node deadCodeElim(Node m) { + // If self is going dead and not being returned here (Nodes returned + // from peephole commonly have no uses (yet)), then kill self. + if( m != this && isUnused() && !isDead() ) { + // Killing self - and since self recursively kills self's inputs we + // might end up killing 'm', which we are returning as a live Node. + // So we add a bogus extra null output edge to stop kill(). + m.keep(); // Keep m alive + kill(); // Kill self because replacing with 'm' + m.unkeep(); // Okay to peephole m + } + return m; + } + + /** + * This function needs to be + * <a href="https://en.wikipedia.org/wiki/Monotonic_function">Monotonic</a> + * as it is part of a Monotone Analysis Framework. + * <a href="https://www.cse.psu.edu/~gxt29/teaching/cse597s21/slides/08monotoneFramework.pdf">See for example this set of slides</a>. + * <p> + * For Chapter 2, all our Types are really integer constants, and so all + * the needed properties are trivially true, and we can ignore the high + * theory. Much later on, this will become important and allow us to do + * many fancy complex optimizations trivially... because theory. + * <p> + * {@link #compute()} needs to be stand-alone, and cannot recursively call compute + * on its inputs, because programs are cyclic (have loops!) and this will just + * infinitely recurse until stack overflow. Instead, compute typically + * computes a new type from the {@link #_type} field of its inputs. + */ + public abstract Type compute(); + + // Set the type. Assert monotonic progress. + // If changing, add users to worklist. + public Type setType(Type type) { + Type old = _type; + assert old==null || type.isa(old); // Since _type not set, can just re-run this in assert in the debugger + if( old == type ) return old; + _type = type; // Set _type late for easier assert debugging + CODE.addAll(_outputs); + moveDepsToWorklist(); + return old; + } + + public <N extends Node> N init() { _type = compute(); return (N)this; } + + /** + * This function rewrites the current Node into a more "idealized" form. + * This is the bulk of our peephole rewrite rules, and we use this to + * e.g. turn arbitrary collections of adds and multiplies with mixed + * constants into a normal form that's easy for hardware to implement. + * Example: An array addressing expression: + * <pre> ary[idx+1]</pre> + * might turn into Sea-of-Nodes IR: + * <pre> (ary+12)+((idx+1) * 4)</pre> + * This expression can then be idealized into: + * <pre> ary + ((idx*4) + (12 + (1*4)))</pre> + * And more folding: + * <pre> ary + ((idx<<2) + 16)</pre> + * And during code-gen: + * <pre> MOV4 Rary,[Ridx<<2 +16] // or some such hardware-specific notation </pre> + * <p> + * {@link #idealize} has a very specific calling convention: + * <ul> + * <li>If NO change is made, return {@code null} + * <li>If ANY change is made, return not-null; this can be {@code this} + * <li>The returned Node does NOT call {@link #peephole} on itself; the {@link #peephole} call will recursively peephole it. + * <li>Any NEW nodes that are not directly returned DO call {@link #peephole}. + * </ul> + * <p> + * Examples: + * <table border="3"> + * <tr><th> before </th><th> after </th><th>return </th><th>comment </th></tr> + * <tr><td>{@code (x+5) }</td><td>{@code (x+5) }</td><td>{@code null }</td><td>No change</td></tr> + * <tr><td>{@code (5+x) }</td><td>{@code (x+5) }</td><td>{@code this }</td><td>Swapped arguments</td></tr> + * <tr><td>{@code ((x+1)+2)}</td><td>{@code (x+(1+2))}</td><td>{@code (x+_) }</td><td>Returns 2 new Nodes</td></tr> + * </table> + * + * The last entry deserves more discussion. The new Node {@code (1+2)} + * created in {@link #idealize} calls {@link #peephole} (which then folds + * into a constant). The other new Node {@code (x+3)} does not call + * peephole, because it is returned and peephole itself will recursively + * call peephole. + * <p> + * Since idealize calls peephole and peephole calls idealize, you must be + * careful that all idealizations are <em>monotonic</em>: all transforms + * remove some feature, so that the set of available transforms always + * shrinks. If you don't, you risk an infinite peephole loop! + * + * @return Either a new or changed node, or null for no changes. + */ + public abstract Node idealize(); + + + // Some of the peephole rules get complex, and search further afield than + // just the nearest neighbor. These peepholes can fail the pattern match + // on a node some distance away, and if that node ever changes we should + // retry the peephole. Track a set of Nodes dependent on `this`, and + // revisit them if `this` changes. + Ary<Node> _deps; + + /** + * Add a node to the list of dependencies. Only add it if its not an input + * or output of this node, that is, it is at least one step away. The node + * being added must benefit from this node being peepholed. + */ + Node addDep( Node dep ) { + // Running peepholes during the big assert cannot have side effects + // like adding dependencies. + if( CODE._midAssert ) return this; + if( dep == null ) return this; + if( _deps==null ) _deps = new Ary<>(Node.class); + if( _deps .find(dep) != -1 ) return this; // Already on list + if( _inputs .find(dep) != -1 ) return this; // No need for deps on immediate neighbors + if( _outputs.find(dep) != -1 ) return this; + _deps.add(dep); + return this; + } + + // Move the dependents onto a worklist, and clear for future dependents. + public void moveDepsToWorklist( ) { + if( _deps==null ) return; + CODE.addAll(_deps); + _deps.clear(); + } + + + // Two nodes are equal if they have the same inputs and the same "opcode" + // which means the same Java class, plus same internal parts. + @Override public final boolean equals( Object o ) { + if( o==this ) return true; + if( o.getClass() != getClass() ) return false; + Node n = (Node)o; + int len = _inputs.size(); + if( len != n._inputs.size() ) return false; + for( int i=0; i<len; i++ ) + if( in(i) != n.in(i) ) + return false; + return eq(n); + } + // Subclasses add extra checks (such as ConstantNodes have same constant), + // and can assume "this!=n" and has the same Java class. + boolean eq( Node n ) { return true; } + + + // Cached hash. If zero, then not computed AND this Node is NOT in the GVN + // table - and can have its edges hacked (which will change his hash + // anyway). If Non-Zero then this Node is IN the GVN table, or is being + // probed to see if it can be inserted. His edges are "locked", because + // hacking his edges will change his hash. + int _hash; + + // If the _hash is set, then the Node is in the GVN table; remove it. + void unlock() { + if( _hash==0 ) return; + Node old = CODE._gvn.remove(this); // Pull from table + assert old==this; + _hash=0; // Out of table now + } + + + // Hash of opcode and inputs + @Override public final int hashCode() { + if( _hash != 0 ) return _hash; + int hash = hash(); + for( Node n : _inputs ) + if( n != null ) + hash = hash ^ (hash<<17) ^ (hash>>13) ^ n._nid; + if( hash==0 ) hash = 0xDEADBEEF; // Bad hash, so use some junky thing + return (_hash=hash); + } + // Subclasses add extra hash info (such as ConstantNodes constant) + int hash() { return 0; } + + // ------------------------------------------------------------------------ + // + + /** Is this Node Memory related */ + public boolean isMem() { return false; } + + /** Pinned in the schedule; these are data nodes whose input#0 is not allowed to change */ + public boolean isPinned() { return false; } + + // Semantic change to the graph (so NOT a peephole), used by the Parser. + // If any input is a float, flip to a float-flavored opcode and widen any + // non-float input. + public final Node widen() { + if( !hasFloatInput() ) return this; + Node flt = copyF(); + if( flt==null ) return this; + for( int i=1; i<nIns(); i++ ) + flt.setDef(i, in(i)._type instanceof TypeFloat ? in(i) : new ToFloatNode(in(i)).peephole()); + kill(); + return flt; + } + private boolean hasFloatInput() { + for( int i=1; i<nIns(); i++ ) + if( in(i)._type instanceof TypeFloat ) + return true; + return false; + } + Node copyF() { return null; } + + + // ------------------------------------------------------------------------ + // Peephole utilities + + // Swap inputs without letting either input go dead during the swap. + Node swap12() { + unlock(); // Hash is order dependent + _inputs.swap(1,2); + return this; + } + + /** + * Does this node contain all constants? Ignores in(0), as is usually + * control. In an input is not a constant, we add dep as a dependency to + * it because dep can make progress if the input becomes a constant later. + * It is sufficient for one of the non-const inputs to have the dependency, + * so we don't bother checking the rest. + */ + boolean allCons(Node dep) { + for( int i=1; i<nIns(); i++ ) + if( !(in(i)._type.isConstant()) ) { + in(i).addDep(dep); // If in(i) becomes a constant later, will trigger some peephole + return false; + } + return true; + } + + + // Make a shallow copy (same class) of this Node, with given inputs and + // empty outputs and a new Node ID. The original inputs are ignored. + // Does not need to be implemented in isCFG() nodes. + Node copy(Node lhs, Node rhs) { throw Utils.TODO("Binary ops need to implement copy"); } + + // Report any post-optimize errors + public Parser.ParseException err() { return null; } + + // Common integer constants + public static ConstantNode con(long x) { return (ConstantNode)(new ConstantNode(TypeInteger.constant(x)).peephole()); } + + // Utility to walk the entire graph applying a function; return the first + // not-null result. + final public <E> E walk( Function<Node,E> pred ) { + assert CODE._visit.isEmpty(); + E rez = _walk(pred); + CODE._visit.clear(); + return rez; + } + + private <E> E _walk( Function<Node,E> pred ) { + if( CODE._visit.get(_nid) ) return null; // Been there, done that + CODE._visit.set(_nid); + E x = pred.apply(this); + if( x != null ) return x; + for( Node def : _inputs ) if( def != null && (x = def._walk(pred)) != null ) return x; + for( Node use : _outputs ) if( use != null && (x = use._walk(pred)) != null ) return x; + return null; + } + + /** + * Debugging utility to find a Node by index + */ + public Node find(int nid) { + return walk( n -> n._nid==nid ? n : null ); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/NotNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/NotNode.java new file mode 100644 index 00000000..b454de66 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/NotNode.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; + +public class NotNode extends Node { + public NotNode(Node in) { super(null, in); } + + @Override public String label() { return "Not"; } + + @Override public String glabel() { return "!"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("(!"), visited); + return sb.append(")"); + } + + @Override + public TypeInteger compute() { + Type t0 = in(1)._type; + if( t0.isHigh() ) return TypeInteger.BOOL.dual(); + if( t0 == Type.NIL || t0 == TypeInteger.ZERO ) return TypeInteger.TRUE; + if( t0 instanceof TypeNil tn && tn.notNull() ) return TypeInteger.FALSE; + if( t0 instanceof TypeInteger i && (i._min > 0 || i._max < 0) ) return TypeInteger.FALSE; + return TypeInteger.BOOL; + } + + @Override + public Node idealize() { return null; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/OrNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/OrNode.java new file mode 100644 index 00000000..9b03dc5d --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/OrNode.java @@ -0,0 +1,55 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +import java.util.BitSet; + +public class OrNode extends LogicalNode { + public OrNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs); } + + @Override public String label() { return "Or"; } + @Override public String op() { return "|"; } + + @Override public String glabel() { return "|"; } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1.isHigh() || t2.isHigh() ) + return TypeInteger.TOP; + if( t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1 ) { + if( i0.isConstant() && i1.isConstant() ) + return TypeInteger.constant(i0.value()|i1.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t1 = lhs._type; + Type t2 = rhs._type; + + // Or of 0. We do not check for (0|x) because this will already + // canonicalize to (x|0) + if( t2.isConstant() && t2 instanceof TypeInteger i && i.value()==0 ) + return lhs; + + // Move constants to RHS: con*arg becomes arg*con + if ( t1.isConstant() && !t2.isConstant() ) + return swap12(); + + // Do we have ((x | (phi cons)) | con) ? + // Do we have ((x | (phi cons)) | (phi cons)) ? + // Push constant up through the phi: x | (phi con0|con0 con1|con1...) + Node phicon = AddNode.phiCon(this,true); + if( phicon!=null ) return phicon; + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new OrNode(_loc,lhs,rhs); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/ParmNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/ParmNode.java new file mode 100644 index 00000000..8d7c4205 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/ParmNode.java @@ -0,0 +1,48 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.Type; +import java.util.BitSet; + +public class ParmNode extends PhiNode { + + // Argument indices are mapped one-to-one on CallNode inputs + public final int _idx; // Argument index + + public ParmNode(String label, int idx, Type declaredType, Node... inputs) { + super(label,declaredType,inputs); + _idx = idx; + } + public ParmNode(ParmNode parm) { super(parm, parm._label, parm._declaredType); _idx = parm._idx; } + + @Override public String label() { return MemOpNode.mlabel(_label); } + + @Override public String glabel() { return _label; } + + public FunNode fun() { return (FunNode)in(0); } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + if( "main".equals(fun()._name) && _label.equals("arg") ) + return sb.append("arg"); + sb.append("Parm_").append(_label).append("("); + for( Node in : _inputs ) { + if (in == null) sb.append("____"); + else in._print0(sb, visited); + sb.append(","); + } + sb.setLength(sb.length()-1); + sb.append(")"); + return sb; + } + + + @Override + public Node idealize() { + if( inProgress() ) return null; + return super.idealize(); + } + + // Always in-progress until we run out of unknown callers + @Override public boolean inProgress() { return fun().inProgress(); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/PhiNode.java new file mode 100644 index 00000000..17748afe --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/PhiNode.java @@ -0,0 +1,201 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; + +public class PhiNode extends Node { + + public final String _label; + + // The Phi type we compute must stay within the domain of the Phi. + // Example Int stays Int, Ptr stays Ptr, Control stays Control, Mem stays Mem. + final Type _declaredType; + + public PhiNode(String label, Type declaredType, Node... inputs) { super(inputs); _label = label; assert declaredType!=null; _declaredType = declaredType; } + public PhiNode(PhiNode phi, String label, Type declaredType) { super(phi); _label = label; _declaredType = declaredType; } + public PhiNode(PhiNode phi) { super(phi); _label = phi._label; _declaredType = phi._declaredType; } + + public PhiNode(RegionNode r, Node sample) { + super(new Node[]{r}); + _label = ""; + _declaredType = sample._type; + while( nIns() < r.nIns() ) + addDef(sample); + } + + @Override public String label() { return "Phi_"+MemOpNode.mlabel(_label); } + + @Override public String glabel() { return "φ_"+_label; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + if( !(region() instanceof RegionNode r) || r.inProgress() ) + sb.append("Z"); + sb.append("Phi("); + for( Node in : _inputs ) { + if (in == null) sb.append("____"); + else in._print0(sb, visited); + sb.append(","); + } + sb.setLength(sb.length()-1); + sb.append(")"); + return sb; + } + + public CFGNode region() { return (CFGNode)in(0); } + @Override public boolean isMultiTail() { return true; } + @Override public boolean isMem() { return _declaredType instanceof TypeMem; } + @Override public boolean isPinned() { return true; } + + @Override + public Type compute() { + if( !(region() instanceof RegionNode r) ) + return region()._type==Type.XCONTROL ? (_type instanceof TypeMem ? TypeMem.TOP : Type.TOP) : _type; + // During parsing Phis have to be computed type pessimistically. + if( r.inProgress() ) return _declaredType; + // Set type to local top of the starting type + Type t = _declaredType.glb().dual();//Type.TOP; + for (int i = 1; i < nIns(); i++) + // If the region's control input is live, add this as a dependency + // to the control because we can be peeped should it become dead. + if( r.in(i).addDep(this)._type != Type.XCONTROL ) + t = t.meet(in(i)._type); + return t; + } + + @Override + public Node idealize() { + if( !(region() instanceof RegionNode r ) ) + return in(1); // Input has collapse to e.g. starting control. + if( r.inProgress() || r.nIns()<=1 ) + return null; // Input is in-progress + + // If we have only a single unique input, become it. + Node live = singleUniqueInput(); + if (live != null) + return live; + + // No bother if region is going to fold dead paths soon + for( int i=1; i<nIns(); i++ ) + if( r.in(i)._type == Type.XCONTROL ) + return null; + + // Pull "down" a common data op. One less op in the world. One more + // Phi, but Phis do not make code. + // Phi(op(A,B),op(Q,R),op(X,Y)) becomes + // op(Phi(A,Q,X), Phi(B,R,Y)). + Node op = in(1); + if( !isMem() && op.nIns()==3 && op.in(0)==null && same_op() ) { + assert !(op instanceof CFGNode); + Node[] lhss = new Node[nIns()]; + Node[] rhss = new Node[nIns()]; + lhss[0] = rhss[0] = in(0); // Set Region + for( int i=1; i<nIns(); i++ ) { + lhss[i] = in(i).in(1); + rhss[i] = in(i).in(2); + } + Node phi_lhs = new PhiNode(_label, _declaredType,lhss).peephole(); + Node phi_rhs = new PhiNode(_label, _declaredType,rhss).peephole(); + Node down = op.copy(phi_lhs,phi_rhs); + // Test not running backwards, which can happen for e.g. And's + if( down.compute().isa(compute()) ) + return down; + down.kill(); + } + + // If merging Phi(N, cast(N)) - we are losing the cast JOIN effects, so just remove. + if( nIns()==3 ) { + if( in(1) instanceof CastNode cast && cast.in(1).addDep(this)==in(2) ) return in(2); + if( in(2) instanceof CastNode cast && cast.in(1).addDep(this)==in(1) ) return in(1); + } + // If merging a null-checked null and the checked value, just use the value. + // if( val ) ..; phi(Region,False=0/null,True=val); + // then replace with plain val. + if( nIns()==3 ) { + int nullx = -1; + if( in(1)._type == in(1)._type.makeZero() ) nullx = 1; + if( in(2)._type == in(2)._type.makeZero() ) nullx = 2; + if( nullx != -1 ) { + Node val = in(3-nullx); + if( val instanceof CastNode cast ) + val = cast.in(1); + if( r.idom(this).addDep(this) instanceof IfNode iff && iff.pred().addDep(this)==val ) { + // Must walk the idom on the null side to make sure we hit False. + CFGNode idom = (CFGNode)r.in(nullx); + while( idom != null && idom.nIns() > 0 && idom.in(0) != iff ) idom = idom.idom(); + if( idom instanceof CProjNode proj && proj._idx==1 ) + return val; + } + } + } + + return null; + } + + private boolean same_op() { + for( int i=2; i<nIns(); i++ ) + if( in(1).getClass() != in(i).getClass() ) + return false; + return true; + } + + /** + * If only single unique input, return it + */ + private Node singleUniqueInput() { + if( region() instanceof LoopNode loop && loop.entry()._type == Type.XCONTROL ) + return null; // Dead entry loops just ignore and let the loop collapse + Node live = null; + for( int i=1; i<nIns(); i++ ) { + // If the region's control input is live, add this as a dependency + // to the control because we can be peeped should it become dead. + if( region().in(i).addDep(this)._type != Type.XCONTROL && in(i) != this ) + if( live == null || live == in(i) ) live = in(i); + else return null; + } + return live; + } + + @Override + boolean allCons(Node dep) { + if( !(region() instanceof RegionNode r) ) return false; + // When the region completes (is no longer in progress) the Phi can + // become a "all constants" Phi, and the "dep" might make progress. + addDep(dep); + if( r.inProgress() ) return false; + return super.allCons(dep); + } + + // True if last input is null + public boolean inProgress() { + return in(nIns()-1) == null; + } + + // Never equal if inProgress + @Override boolean eq( Node n ) { + return !inProgress(); + } + + @Override + public Parser.ParseException err() { + if( _type != Type.BOTTOM ) return null; + + // BOTTOM means we mixed e.g. int and ptr + for( int i=1; i<nIns(); i++ ) + // Already an error, but better error messages come from elsewhere + if( in(i)._type == Type.BOTTOM ) + return null; + + // Gather a minimal set of types that "cover" all the rest + boolean ti=false, tf=false, tp=false, tn=false; + for( int i=1; i<nIns(); i++ ) { + Type t = in(i)._type; + ti |= t instanceof TypeInteger x; + tf |= t instanceof TypeFloat x; + tp |= t instanceof TypeMemPtr x; + tn |= t==Type.NIL; + } + return ReturnNode.mixerr(ti,tf,tp,tn, ((RegionNode)region())._loc); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/ProjNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/ProjNode.java new file mode 100644 index 00000000..ef7b17e4 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/ProjNode.java @@ -0,0 +1,55 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeTuple; + +import java.util.BitSet; + +public class ProjNode extends Node { + + // Which slice of the incoming multipart value + public final int _idx; + + // Debugging label + public final String _label; + + public ProjNode(Node ctrl, int idx, String label) { + super(new Node[]{ctrl}); + _idx = idx; + _label = label; + } + public ProjNode(ProjNode p) { super(p); _idx = p._idx; _label = p._label; } + + @Override public String label() { return _label; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + if( _label != null ) return sb.append(_label); + if( in(0) instanceof CallEndNode cend && cend.call()!=null ) + return cend.call()._print0(sb,visited); + return sb.append("LONELY PROJ"); + } + + @Override public CFGNode cfg0() { + return in(0) instanceof CFGNode cfg ? cfg : in(0).cfg0(); + } + + @Override public boolean isMultiTail() { return in(0).isMultiHead(); } + @Override public boolean isMem() { return _type instanceof TypeMem; } + @Override public boolean isPinned() { return true; } + + @Override + public Type compute() { + Type t = in(0)._type; + return t instanceof TypeTuple tt ? tt._types[_idx] : Type.BOTTOM; + } + + @Override public Node idealize() { return ((MultiNode)in(0)).pcopy(_idx); } + + @Override + boolean eq( Node n ) { return _idx == ((ProjNode)n)._idx; } + + @Override + int hash() { return _idx; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java new file mode 100644 index 00000000..b755af3f --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java @@ -0,0 +1,30 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMemPtr; + +import java.util.BitSet; + +/** + * Cast a pointer to read-only + */ +public class ReadOnlyNode extends Node { + public ReadOnlyNode( Node n ) { super(null,n); } + public ReadOnlyNode( ReadOnlyNode n ) { super(n); } + @Override public String label() { return "ReadOnly"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return in(1)._print0(sb.append("(const)"),visited); + } + @Override public Type compute() { + Type t = in(1)._type; + return t instanceof TypeMemPtr tmp ? tmp.makeRO() : t; + } + + @Override public Node idealize() { + if( in(1)._type instanceof TypeMemPtr tmp && tmp.isFinal() ) + return in(1); + return null; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/RegionNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/RegionNode.java new file mode 100644 index 00000000..5d3db5c6 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/RegionNode.java @@ -0,0 +1,126 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.type.Type; +import java.util.*; + +public class RegionNode extends CFGNode { + // Source location for late discovered errors + public Parser.Lexer _loc; + + public RegionNode(Parser.Lexer loc, Node... nodes) { super(nodes); _loc = loc; } + public RegionNode(RegionNode r, Parser.Lexer loc) { super(r); _loc = loc; } + public RegionNode(RegionNode r) { super(r); _loc = r._loc; } + + @Override + public String label() { return "Region"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return sb.append(label()); + } + + @Override public boolean isMultiHead() { return true; } + @Override public boolean blockHead() { return true; } + + @Override + public Type compute() { + if( inProgress() ) return Type.CONTROL; + Type t = Type.XCONTROL; + for (int i = 1; i < nIns(); i++) + t = t.meet(in(i)._type); + return t; + } + + @Override + public Node idealize() { + if( inProgress() ) return null; + + Node progress = deadPath(); + if( progress!=null ) return progress; + + // If down to a single input, become that input + if( nIns()==2 && !hasPhi() ) + return in(1); // Collapse if no Phis; 1-input Phis will collapse on their own + + // If a CFG diamond with no merging, delete: "if( pred ) {} else {};" + if( !hasPhi() && // No Phi users, just a control user + in(1) instanceof CProjNode p1 && + in(2) instanceof CProjNode p2 && + p1.in(0).addDep(this)==p2.in(0).addDep(this) && + p1.in(0) instanceof IfNode iff ) { + // Replace with the iff.ctrl directly + if( nIns()==3 ) return iff.ctrl(); + // Just delete the path for fat Regions + setDef(1,iff.ctrl()); + return delDef(2); + } + + return null; + } + + Node deadPath() { + // Delete dead paths into a Region + int path = findDeadInput(); + if( path==0 ) return null; + // Do not delete the entry path of a loop (ok to remove the back edge + // and make the loop a single-entry Region which folds away the Loop). + // Folding the entry path confused the loop structure, moving the + // backedge to the entry point. + if( this instanceof LoopNode loop && loop.entry()==in(path) ) + return null; + // Cannot use the obvious output iterator here, because a Phi deleting + // an input might recursively delete *itself*. This shuffles the + // output array, and we might miss iterating an unrelated Phi. So on + // rare occasions we repeat the loop to get all the Phis. + int nouts = 0; + while( nouts != nOuts() ) { + nouts = nOuts(); + for( int i=0; i<nOuts(); i++ ) + if( out(i) instanceof PhiNode phi && phi.nIns()==nIns() ) + CodeGen.CODE.addAll(phi.delDef(path)._outputs); + } + return isDead() ? Parser.XCTRL : delDef(path); + } + + + private int findDeadInput() { + for( int i=1; i<nIns(); i++ ) + if( in(i)._type==Type.XCONTROL ) + return i; + return 0; // All inputs alive + } + + boolean hasPhi() { + for( Node phi : _outputs ) + if( phi instanceof PhiNode ) + return true; + return false; + } + + // Immediate dominator of Region is a little more complicated. + @Override public int idepth() { + if( _idepth!=0 ) return _idepth; + int d=0; + for( Node n : _inputs ) + if( n!=null ) + d = Math.max(d,((CFGNode)n).idepth()+1); + return _idepth=d; + } + + @Override public CFGNode idom(Node dep) { + CFGNode lca = null; + // Walk the LHS & RHS idom trees in parallel until they match, or either fails. + // Because this does not cache, it can be linear in the size of the program. + for( int i=1; i<nIns(); i++ ) + lca = cfg(i)._idom(lca,dep); + return lca; + } + + // True if last input is null + public boolean inProgress() { return nIns()>1 && in(nIns()-1) == null; } + + // Never equal if inProgress + @Override boolean eq( Node n ) { return !inProgress(); } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/ReturnNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/ReturnNode.java new file mode 100644 index 00000000..20b6c7b9 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/ReturnNode.java @@ -0,0 +1,129 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; + +/** + * The Return node has two inputs. The first input is a control node and the + * second is the data node that supplies the return value. + * <p> + * In this presentation, Return functions as a Stop node, since multiple <code>return</code> statements are not possible. + * The Stop node will be introduced in Chapter 6 when we implement <code>if</code> statements. + * <p> + * The Return's output is the value from the data node. + */ +public class ReturnNode extends CFGNode { + + public FunNode _fun; + + public ReturnNode(Node ctrl, Node mem, Node data, Node rpc, FunNode fun ) { + super(ctrl, mem, data, rpc); + _fun = fun; + } + public ReturnNode( ReturnNode ret, FunNode fun ) { super(ret); _fun = fun; } + + public Node ctrl() { return in(0); } + public Node mem () { return in(1); } + public Node expr() { return in(2); } + public Node rpc () { return in(3); } + public FunNode fun() { return _fun; } + + @Override + public String label() { return "Return"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("return "); + expr()._print0(sb, visited); + return sb.append(";"); + } + + // No one unique control follows; can be many call end sites + @Override public CFGNode uctrl() { return null; } + + @Override + public Type compute() { + if( inProgress () ) return TypeTuple.RET; // In progress + if( _fun.isDead() ) return TypeTuple.RET.dual(); // Dead another way + return TypeTuple.make(ctrl()._type,mem()._type,expr()._type); + } + + @Override public Node idealize() { + if( inProgress () ) return null; + if( _fun.isDead() ) return null; + + // Upgrade signature based on return type + Type ret = expr()._type; + TypeFunPtr fcn = _fun.sig(); + assert ret.isa(fcn.ret()); + if( ret != fcn.ret() ) + _fun.setSig(fcn.makeFrom(ret)); + + // If dead (cant be reached; infinite loop), kill the exit values + if( ctrl()._type==Type.XCONTROL && + !(mem() instanceof ConstantNode && expr() instanceof ConstantNode) ) { + Node top = new ConstantNode(Type.TOP).peephole(); + setDef(1,top); + setDef(2,top); + return this; + } + + return null; + } + + public boolean inProgress() { + return ctrl().getClass() == RegionNode.class && ((RegionNode)ctrl()).inProgress(); + } + + // Gather parse-time return types for error reporting + private Type mt = Type.TOP; + private boolean ti=false, tf=false, tp=false, tn=false; + + // Add a return exit to the current parsing function + void addReturn( Node ctrl, Node rmem, Node expr ) { + assert inProgress(); + + // Gather parse-time return types for error reporting + Type t = expr._type; + mt = mt.meet(t); + ti |= t instanceof TypeInteger x; + tf |= t instanceof TypeFloat x; + tp |= t instanceof TypeMemPtr x; + tn |= t==Type.NIL; + + // Merge path into the One True Return + RegionNode r = (RegionNode)ctrl(); + // Assert that the Phis are in particular outputs; not reordered or shuffled + PhiNode mem = (PhiNode)r.out(0); assert mem._declaredType == TypeMem.BOT; + PhiNode rez = (PhiNode)r.out(1); assert rez._declaredType == Type.BOTTOM; + // Pop "inProgress" null off + r ._inputs.pop(); + mem._inputs.pop(); + rez._inputs.pop(); + // Add new return point + r .addDef(ctrl); + mem.addDef(rmem); + rez.addDef(expr); + // Back to being inProgress + r .addDef(null); + mem.addDef(null); + rez.addDef(null); + } + + @Override public Parser.ParseException err() { + return expr()._type/*mt*/==Type.BOTTOM ? mixerr(ti,tf,tp,tn,_fun._loc) : null; + } + + static Parser.ParseException mixerr( boolean ti, boolean tf, boolean tp, boolean tn, Parser.Lexer loc ) { + if( !ti && !tf && !tp && !tn ) + return Parser.error("No defined return type", loc); + SB sb = new SB().p("No common type amongst "); + if( ti ) sb.p("int and "); + if( tf ) sb.p("f64 and "); + if( tp || tn ) sb.p("reference and "); + return Parser.error(sb.unchar(5).toString(),loc); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/RoundF32Node.java b/chapter20/src/main/java/com/seaofnodes/simple/node/RoundF32Node.java new file mode 100644 index 00000000..44d3a587 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/RoundF32Node.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; + +import java.util.BitSet; + +public class RoundF32Node extends Node { + public RoundF32Node(Node lhs) { super(null, lhs); } + + @Override public String label() { return "RoundF32"; } + + @Override public String glabel() { return "(f32)"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return in(1)._print0(sb.append("((f32)"), visited).append(")"); + } + + @Override + public Type compute() { + if (in(1)._type instanceof TypeFloat i0 && i0.isConstant() ) + return TypeFloat.constant((float)i0.value()); + return in(1)._type; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Type t1 = lhs._type; + + // RoundF32 of float + if( t1 instanceof TypeFloat tf && tf._sz==32 ) + return lhs; + + return null; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/SarNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/SarNode.java new file mode 100644 index 00000000..4a71ec06 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/SarNode.java @@ -0,0 +1,50 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; + +public class SarNode extends LogicalNode { + public SarNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs); } + + @Override public String label() { return "Sar"; } + @Override public String op() { return ">>"; } + + @Override public String glabel() { return ">>"; } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1.isHigh() || t2.isHigh() ) + return TypeInteger.TOP; + if (t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1) { + if( i0 == TypeInteger.ZERO ) + return TypeInteger.ZERO; + if( i0.isConstant() && i1.isConstant() ) + return TypeInteger.constant(i0.value()>>i1.value()); + if( i1.isConstant() ) { + int log = (int)i1.value(); + return TypeInteger.make(-1L<<(63-log),(1L<<(63-log))-1); + } + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t2 = rhs._type; + + // Sar of 0. + if( t2.isConstant() && t2 instanceof TypeInteger i && (i.value()&63)==0 ) + return lhs; + + // TODO: x >> 3 >> (y ? 1 : 2) ==> x >> (y ? 4 : 5) + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new SarNode(_loc,lhs,rhs); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/ScopeNode.java new file mode 100644 index 00000000..1208ff63 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -0,0 +1,428 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.type.*; +import java.util.*; + +/** + * The Scope node is purely a parser helper - it tracks names to nodes with a + * stack of variables. + */ +public class ScopeNode extends MemMergeNode { + + /** + * The control is a name that binds to the currently active control + * node in the graph + */ + public static final String CTRL = "$ctrl"; + public static final String ARG0 = "arg"; + public static final String MEM0 = "$mem"; + + // All active/live variables in all nested scopes, all run together + public final Ary<Var> _vars; + + // Size of each nested lexical scope + public final Ary<Integer> _lexSize; + + // Lexical scope is one of normal Block, constructor or function + public enum Kind { Block, Constructor, Function }; + public final Ary<Kind> _kinds; + + // Extra guards; tested predicates and casted results + private final Ary<Node> _guards; + + // A new ScopeNode + public ScopeNode() { + super(true); + _vars = new Ary<>(Var .class); + _lexSize= new Ary<>(Integer.class); + _kinds = new Ary<>(Kind .class); + _guards = new Ary<>(Node .class); + } + + @Override public String label() { return "Scope"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + sb.append("Scope[ "); + int j=1; + for( int i=0; i<nIns(); i++ ) { + if( j < _lexSize._len && i == _lexSize.at(j) ) { sb.append("| "); j++; } + Var v = _vars.get(i); + sb.append(v.type().print(new SB())); + sb.append(" "); + if( v._final ) sb.append("!"); + sb.append(v._name); + sb.append("="); + Node n = in(i); + while( n instanceof ScopeNode loop ) { + sb.append("Lazy_"); + n = loop.in(i); + } + if( n==null ) sb.append("___"); + else n._print0(sb, visited); + sb.append(", "); + } + sb.setLength(sb.length()-2); + return sb.append("]"); + } + + + public Node ctrl() { return in(0); } + public MemMergeNode mem() { return (MemMergeNode)in(1); } + + /** + * The ctrl of a ScopeNode is always bound to the currently active + * control node in the graph, via a special name '$ctrl' that is not + * a valid identifier in the language grammar and hence cannot be + * referenced in Simple code. + * + * @param n The node to be bound to '$ctrl' + * + * @return Node that was bound + */ + public <N extends Node> N ctrl(N n) { return setDef(0,n); } + public Node mem(Node n) { return setDef(1,n); } + + public void push(Kind kind) { + assert _lexSize._len==_kinds._len; + _lexSize.push(_vars.size()); + _kinds .push(kind); + } + + // Pop a lexical scope + public void pop() { + assert _lexSize._len==_kinds._len; + promote(); + int n = _lexSize.pop(); + _kinds.pop(); + popUntil(n); + _vars.setLen(n); + } + + + // Look for forward references in the last lexical scope and promote to the + // next outer lexical scope. At the last scope declare them an error. + public void promote() { + int n = _lexSize.last(); + for( int i=n; i<nIns(); i++ ) { + Var v = _vars.at(i); + if( !v.isFRef() ) continue; + if( _lexSize._len==1 ) + throw Parser.error("Undefined name '" + v._name + "'",v._loc); + _vars.swap(n,i); + _inputs.swap(n,i); + v._idx = n; + n++; + _lexSize.set(_lexSize._len-1,n); + } + } + + + public boolean inCon() { return _kinds.last() == Kind.Constructor; } + + // Is v outside any current function scope? + public boolean outOfFunction( Var v ) { + for( int i=_lexSize._len-1; i>=0 && v._idx<_lexSize.at(i); i-- ) + if( _kinds.at(i)==Kind.Function ) + return true; + return false; + } + + + // Find name in reverse, return an index into _vars or -1. Linear scan + // instead of hashtable, but probably doesn't matter until the scan + // typically hits many dozens of variables. + int find( String name ) { + for( int i=_vars.size()-1; i>=0; i-- ) + if( _vars.at(i)._name.equals(name) ) + return i; + return -1; + } + + /** + * Create a new variable name in the current scope + */ + public boolean define( String name, Type declaredType, boolean xfinal, Node init, Parser.Lexer loc ) { + assert _lexSize.isEmpty() || name.charAt(0)!='$' ; // Later scopes do not define memory + if( _lexSize._len > 0 ) + for( int i=_vars.size()-1; i>=_lexSize.last(); i-- ) { + Var n = _vars.at(i); + if( n._name.equals(name) ) { + if( !n.isFRef() ) return false; // Double define + FRefNode fref = (FRefNode)in(n._idx); // Get forward ref + if( !xfinal || !declaredType.isConstant() ) throw fref.err(); // Must be a final constant + n.defFRef(declaredType,xfinal,loc); // Declare full correct type, final, source location + setDef(n._idx,fref.addDef(init)); // Set FRef to defined; tell parser also + } + } + Var v = new Var(nIns(),name,declaredType,xfinal,loc,init==Parser.XCTRL); + _vars.add(v); + // Creating a forward reference + if( init==Parser.XCTRL ) + init = new FRefNode(v).init(); + addDef(init); + return true; + } + + // Read from memory + public Node mem( int alias ) { return mem()._mem(alias,null); } + // Write to memory + public void mem( int alias, Node st ) { mem()._mem(alias,st); } + + + /** + * Lookup a name in all scopes starting from most deeply nested. + * + * @param name Name to be looked up + * @return null if not found, or the implementing Node + */ + public Var lookup( String name ) { + int idx = find(name); + // -1 is missed in all scopes, not found + return idx == -1 ? null : update(_vars.at(idx),null); + } + + /** + * If the name is present in any scope, then redefine else null + * + * @param name Name being redefined + * @param n The node to bind to the name + */ + public void update( String name, Node n ) { + int idx = find(name); + assert idx>=0; + update(_vars.at(idx),n); + } + + public Var update( Var v, Node st ) { + Node old = in(v._idx); + if( old instanceof ScopeNode loop ) { + // Lazy Phi! + Node def = loop.in(v._idx); + old = def instanceof PhiNode phi && loop.ctrl()==phi.region() + // Loop already has a real Phi, use it + ? def + // Set real Phi in the loop head + // The phi takes its one input (no backedge yet) from a recursive + // lookup, which might have insert a Phi in every loop nest. + : loop.setDef(v._idx,new PhiNode(v._name, v.lazyGLB(), loop.ctrl(), loop.in(loop.update(v,null)._idx),null).peephole()); + setDef(v._idx,old); + } + assert !v._final || st==null; + if( st!=null ) setDef(v._idx,st); // Set new value + return v; + } + + /** + * Duplicate a ScopeNode; including all levels, up to Nodes. So this is + * neither shallow (would dup the Scope but not the internal HashMap + * tables), nor deep (would dup the Scope, the HashMap tables, but then + * also the program Nodes). + * <p> + * If the {@code loop} flag is set, the edges are filled in as the original + * Scope, as an indication of Lazy Phis at loop heads. The goal here is to + * not make Phis at loop heads for variables which are never touched in the + * loop body. + * <p> + * The new Scope is a full-fledged Node with proper use<->def edges. + */ + public ScopeNode dup() { return dup(false); } + public ScopeNode dup(boolean loop) { + ScopeNode dup = new ScopeNode(); + // Our goals are: + // 1) duplicate the name bindings of the ScopeNode across all stack levels + // 2) Make the new ScopeNode a user of all the nodes bound + // 3) Ensure that the order of defs is the same to allow easy merging + dup._vars .addAll(_vars ); + dup._lexSize.addAll(_lexSize); + dup._kinds .addAll(_kinds ); + dup._guards .addAll(_guards ); + // The dup'd guards all need dup'd keepers, to keep proper accounting + // when later removing all guards + for( Node n : _guards ) + if( !(n instanceof CFGNode) ) + n.keep(); + dup.addDef(ctrl()); // Control input is just copied + + // Memory input is a shallow copy + MemMergeNode memdup = new MemMergeNode(true), mem = mem(); + memdup.addDef(null); + memdup.addDef(loop ? this : mem.in(1)); + for( int i=2; i<mem.nIns(); i++ ) + // For lazy phis on loops we use a sentinel + // that will trigger phi creation on update + memdup.addDef(loop ? this : mem.in(i)); + dup.addDef(memdup); + + // Copy of other inputs + for( int i=2; i<nIns(); i++ ) + // For lazy phis on loops we use a sentinel + // that will trigger phi creation on update + dup.addDef(loop && !_vars.at(i)._final ? this : in(i)); + return dup; + } + + /** + * Merges the names whose node bindings differ, by creating Phi node for such names + * The names could occur at all stack levels, but a given name can only differ in the + * innermost stack level where the name is bound. + * + * @param that The ScopeNode to be merged into this + * @return A new node representing the merge point + */ + public RegionNode mergeScopes(ScopeNode that, Parser.Lexer loc) { + RegionNode r = ctrl(new RegionNode(loc,null,ctrl(), that.ctrl()).keep()); + mem()._merge(that.mem(),r); + this ._merge(that ,r); + that.kill(); // Kill merged scope + CodeGen.CODE.add(r); + return r.unkeep(); + } + + private void _merge(ScopeNode that, RegionNode r) { + for( int i = 2; i < nIns(); i++) + if( in(i) != that.in(i) ) { // No need for redundant Phis + // If we are in lazy phi mode we need to a lookup + // by name as it will trigger a phi creation + Var v = _vars.at(i); + Node lhs = this.in(this.update(v,null)); + Node rhs = that.in(that.update(v,null)); + setDef(i, new PhiNode(v._name, v.type(), r, lhs, rhs).peephole()); + } + } + + // Balance arms of an IF. Extra lonely defs are thrown: "if(pred) int x;". + // Forward refs are copied to the other side, "as if" they were there all along. + public void balanceIf( ScopeNode scope ) { + for( int i = nIns(); i < scope.nIns(); i++ ) { + Var n = scope._vars.at(i); + if( n.isFRef() ) { // RHS has forward refs + _vars.add(n); // Copy to LHS + addDef(scope.in(i)); + } else + throw Parser.error("Cannot define a '"+n._name+"' on one arm of an if",n._loc); + } + } + + + // peephole the backedge scope into this loop head scope + // We set the second input to the phi from the back edge (i.e. loop body) + public void endLoop(ScopeNode back, ScopeNode exit ) { + Node ctrl = ctrl(); + assert ctrl instanceof LoopNode loop && loop.inProgress(); + ctrl.setDef(2,back.ctrl()); + + mem()._endLoopMem( this, back.mem(), exit.mem() ); + this ._endLoop ( this, back , exit ); + back.kill(); // Loop backedge is dead + // Now one-time do a useless-phi removal + mem()._useless(); + this ._useless(); + + // The exit mem's lazy default value had been the loop top, + // now it goes back to predating the loop. + exit.mem().setDef(1,mem().in(1)); + } + + // Fill in the backedge of any inserted Phis + void _endLoop( ScopeNode scope, Node back, Node exit ) { + for( int i=2; i<nIns(); i++ ) { + if( _vars.at(i)._final ) continue; // Final vars did not get modified in the loop + if( _vars.at(i).type().isHighOrConst() ) continue; // Cannot lift higher than a constant, so no Phi + if( back.in(i) != scope ) { + PhiNode phi = (PhiNode)in(i); + assert phi.region()==scope.ctrl() && phi.in(2)==null; + phi.setDef(2,back.in(i)); // Fill backedge + } + if( exit.in(i) == scope ) // Replace a lazy-phi on the exit path also + exit.setDef(i,in(i)); + } + } + + + // Up-casting: using the results of an If to improve a value. + // E.g. "if( ptr ) ptr.field;" is legal because ptr is known not-null. + public void addGuards( Node ctrl, Node pred, boolean invert ) { + assert ctrl instanceof CFGNode; + _guards.add(ctrl); // Marker to distinguish 0,1,2 guards + // add pred & its cast to the normal input list, with special Vars + if( pred==null || pred.isDead() ) + return; // Dead, do not add any guards + // Invert the If conditional + if( invert ) + pred = pred instanceof NotNode not ? not.in(1) : CodeGen.CODE.add(new NotNode(pred).peephole()); + // This is a zero/null test. + // Compute the positive test type. + Type tnz = pred._type.nonZero(); + if( tnz!=null ) + _addGuard(tnz,ctrl,pred); + + // Compute the negative test type. + if( pred instanceof NotNode not ) { + Node npred = not.in(1); + Type tzero = npred._type.makeZero(); + _addGuard(tzero,ctrl,npred); + } + } + + private void _addGuard(Type guard, Node ctrl, Node pred) { + Type tcast = guard.join(pred._type); + if( tcast != pred._type && !tcast.isHigh() ) { + Node cast = new CastNode(tcast,ctrl,pred.keep()).peephole().keep(); + _guards.add(pred); + _guards.add(cast); + replace(pred,cast); + } + } + + + // Remove matching pred/cast pairs from this guarded region. + public ScopeNode removeGuards( Node ctrl ) { + assert ctrl instanceof CFGNode; + // 0,1 or 2 guards + while( true ) { + Node g = _guards.pop(); + if( g == ctrl ) break; + if( g instanceof CFGNode ) continue; + g .unkill(); // Pop/kill cast + _guards.pop().unkill(); // Pop/kill pred + } + return this; + } + + // If we find a guarded instance of pred, replace with the upcasted version + public Node upcastGuard( Node pred ) { + // If finding an instanceof pred, replace with cast. + // Otherwise, just pred itself. + int i = _guards._len; + while( i > 0 ) { + Node cast = _guards.at(--i); + if( cast instanceof CFGNode ) continue; // Marker between guard sets + Node xpred = _guards.at(--i); + if( xpred == pred ) + return cast; + } + return pred; + } + + // Kill guards also + @Override public void kill() { + for( Node n : _guards ) + if( !(n instanceof CFGNode) ) + n.unkill(); + _guards.clear(); + // Can have lazy uses remaining + if( isUnused() ) + super.kill(); + } + + + private void replace( Node old, Node cast ) { + assert old!=null && old!=cast; + for( int i=0; i<nIns(); i++ ) + if( in(i)==old ) + setDef(i,cast); + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/ShlNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/ShlNode.java new file mode 100644 index 00000000..21dbd503 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/ShlNode.java @@ -0,0 +1,53 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; + +public class ShlNode extends LogicalNode { + public ShlNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs); } + + @Override public String label() { return "Shl"; } + @Override public String op() { return "<<"; } + + @Override public String glabel() { return "<<"; } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1.isHigh() || t2.isHigh() ) + return TypeInteger.TOP; + if (t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1 ) { + if( i0 == TypeInteger.ZERO ) + return TypeInteger.ZERO; + if( i0.isConstant() && i1.isConstant() ) + return TypeInteger.constant(i0.value()<<i1.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + + if( rhs._type instanceof TypeInteger shl && shl.isConstant() ) { + // Shl of 0. + if( (shl.value()&63)==0 ) + return lhs; + // (x + c) << i => (x << i) + (c << i) + if( lhs instanceof AddNode add && add.addDep(this).in(2)._type instanceof TypeInteger c && c.isConstant() ) { + long sum = c.value() << shl.value(); + if( Integer.MIN_VALUE <= sum && sum <= Integer.MAX_VALUE ) + return new AddNode(new ShlNode(_loc,add.in(1),rhs).peephole(), Parser.con(sum) ); + } + } + + // TODO: x << 3 << (y ? 1 : 2) ==> x << (y ? 4 : 5) + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new ShlNode(_loc,lhs,rhs); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/ShrNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/ShrNode.java new file mode 100644 index 00000000..789fb8a8 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/ShrNode.java @@ -0,0 +1,46 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; + +public class ShrNode extends LogicalNode { + public ShrNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs); } + + @Override public String label() { return "Shr"; } + @Override public String op() { return ">>>"; } + + @Override public String glabel() { return ">>>"; } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1.isHigh() || t2.isHigh() ) + return TypeInteger.TOP; + if (t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1 ) { + if( i0 == TypeInteger.ZERO ) + return TypeInteger.ZERO; + if( i0.isConstant() && i1.isConstant() ) + return TypeInteger.constant(i0.value()>>>i1.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t2 = rhs._type; + + // Shr of 0. + if( t2.isConstant() && t2 instanceof TypeInteger i && (i.value()&63)==0 ) + return lhs; + + // TODO: x >>> 3 >>> (y ? 1 : 2) ==> x >>> (y ? 4 : 5) + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new ShrNode(_loc,lhs,rhs); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/StartNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/StartNode.java new file mode 100644 index 00000000..26e08535 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/StartNode.java @@ -0,0 +1,58 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.*; +import java.util.Arrays; +import java.util.BitSet; +import java.util.HashSet; +import static com.seaofnodes.simple.Utils.TODO; + +/** + * The Start node represents the start of the function. + * + * Start initially has 1 input (arg) from outside and the initial control. + * In ch10 we also add mem aliases as structs get defined; each field in struct + * adds a distinct alias to Start's tuple. + */ +public class StartNode extends LoopNode implements MultiNode { + + final Type _arg; + + public StartNode(Type arg) { super((Parser.Lexer)null,null); _arg = arg; _type = compute(); } + public StartNode(StartNode start) { super(start); _arg = start._arg; } + + @Override public String label() { return "Start"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return sb.append(label()); + } + + @Override public boolean isMultiHead() { return true; } + @Override public boolean blockHead() { return true; } + @Override public CFGNode cfg0() { return null; } + + // Get the one control following; error to call with more than one e.g. an + // IfNode or other multi-way branch. For Start, its "main" + @Override public CFGNode uctrl() { + // Find "main", its the start. + CFGNode C = null; + for( Node use : _outputs ) + if( use instanceof FunNode fun && fun.sig().isa(CodeGen.CODE._main) ) + { assert C==null; C = fun; } + return C; + } + + + @Override public TypeTuple compute() { + return TypeTuple.make(Type.CONTROL,TypeMem.TOP,_arg); + } + + @Override public Node idealize() { return null; } + + // No immediate dominator, and idepth==0 + @Override public int idepth() { return 0; } + @Override public CFGNode idom(Node dep) { return null; } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/StopNode.java new file mode 100644 index 00000000..6aaba3b3 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -0,0 +1,64 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.type.Type; +import java.util.BitSet; + +public class StopNode extends CFGNode { + + public final String _src; + + public StopNode(String src) { + super(); + _src = src; + _type = compute(); + } + public StopNode(StopNode stop) { super(stop); _src = stop._src; } + + @Override + public String label() { + return "Stop"; + } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + // For the sake of many old tests, and single value prints as "return val" + if( ret()!=null ) return ret()._print0(sb,visited); + sb.append("Stop[ "); + for( Node ret : _inputs ) + ret._print0(sb, visited).append(" "); + return sb.append("]"); + } + + @Override public boolean blockHead() { return true; } + + + // If a single Return, return it. + // Otherwise, null because ambiguous. + public ReturnNode ret() { + return nIns()==1 && in(0) instanceof ReturnNode ret ? ret : null; + } + + @Override + public Type compute() { + return Type.BOTTOM; + } + + @Override + public Node idealize() { + int len = nIns(); + for( int i=0; i<nIns(); i++ ) + if( ((ReturnNode)in(i)).fun().isDead() ) + delDef(i--); + if( len != nIns() ) return this; + return null; + } + + @Override public int idepth() { + if( _idepth!=0 ) return _idepth; + int d=0; + for( Node ret : _inputs ) + d = Math.max(d,((ReturnNode)ret).idepth()+1); + return _idepth=d; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/StoreNode.java new file mode 100644 index 00000000..93294dba --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/StoreNode.java @@ -0,0 +1,113 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; + +/** + * Store represents setting a value to a memory based object, in chapter 10 + * this means a field inside a struct. + */ +public class StoreNode extends MemOpNode { + + private final boolean _init; // Initializing writes are allowed to write null + + /** + * @param name The struct field we are assigning to + * @param mem The memory alias node - this is updated after a Store + * @param ptr The ptr to the struct base where we will store a value + * @param off The offset inside the struct base + * @param value Value to be stored + */ + public StoreNode(Parser.Lexer loc, String name, int alias, Type glb, Node mem, Node ptr, Node off, Node value, boolean init) { + super(loc, name, alias, glb, mem, ptr, off, value); + _init = init; + } + + // GraphVis DOT code and debugger labels + @Override public String label() { return "st_"+mlabel(); } + // GraphVis node-internal labels + @Override public String glabel() { return "." +_name+"="; } + @Override public boolean isMem() { return true; } + + public Node val() { return in(4); } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return sb.append(".").append(_name).append("=").append( val()).append(";"); + } + + @Override + public Type compute() { + Type val = val()._type; + TypeMem mem = (TypeMem)mem()._type; // Invariant + if( mem == TypeMem.TOP ) return TypeMem.TOP; + Type t = Type.BOTTOM; // No idea on field contents + // Same alias, lift val to the declared type and then meet into other fields + if( mem._alias == _alias ) { + // Update declared forward ref to the actual + if( _declaredType.isFRef() && val instanceof TypeMemPtr tmp && !tmp.isFRef() ) + _declaredType = tmp; + val = val.join(_declaredType); + t = val.meet(mem._t); + } + return TypeMem.make(_alias,t); + } + + @Override + public Node idealize() { + + // Simple store-after-store on same address. Should pick up the + // required init-store being stomped by a first user store. + if( mem() instanceof StoreNode st && + ptr()==st.ptr() && // Must check same object + off()==st.off() && // And same offset (could be "same alias" but this handles arrays to same index) + ptr()._type instanceof TypeMemPtr && // No bother if weird dead pointers + // Must have exactly one use of "this" or you get weird + // non-serializable memory effects in the worse case. + checkOnlyUse(st) ) { + assert _name.equals(st._name); // Equiv class aliasing is perfect + setDef(1,st.mem()); + return this; + } + + // Value is automatically truncated by narrow store + if( val() instanceof AndNode and && and.in(2)._type.isConstant() ) { + int log = _declaredType.log_size(); + if( log<3 ) { // And-mask vs narrow store + long mask = ((TypeInteger)and.in(2)._type).value(); + long bits = (1L<<(8<<log))-1; + // Mask does not mask any of the stored bits + if( (bits&mask)==bits ) + // So and-mask is already covered by the store + { setDef(4,and.in(1)); return this; } + } + } + + + return null; + } + + // Check that "mem" has no uses except "this" + private boolean checkOnlyUse(Node mem) { + if( mem.nOuts()==1 ) return true; + // Add deps on the other uses (can be e.g. ScopeNode mid-parse) so that + // when the other uses go away we can retry. + for( Node use : mem._outputs ) + if( use != this ) + use.addDep(this); + return false; + } + + @Override + public Parser.ParseException err() { + Parser.ParseException err = super.err(); + if( err != null ) return err; + TypeMemPtr tmp = (TypeMemPtr)ptr()._type; + if( tmp._obj.field(_name)._final && !_init ) + return Parser.error("Cannot modify final field '"+_name+"'",_loc); + Type t = val()._type; + //return _init || t.isa(_declaredType) ? null : Parser.error("Cannot store "+t+" into field "+_declaredType+" "+_name,_loc); + return null; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/StructNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/StructNode.java new file mode 100644 index 00000000..076b342a --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/StructNode.java @@ -0,0 +1,46 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; + +/** + * Build a compound object + */ +public class StructNode extends Node { + + public TypeStruct _ts; + + @Override public String label() { return _ts==null ? "STRUCT?" : _ts.str(); } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + if( _ts==null ) return sb.append("STRUCT?"); + sb.append(_ts._name).append(" {"); + for( int i=0; i<nIns(); i++ ) { + sb.append(_ts._fields[i]._fname).append(":"); + sb.append((in(i)==null ? Type.BOTTOM : in(i)._type).print(new SB())); + sb.append("; "); + } + sb.setLength(sb.length()-2); + return sb.append("}"); + } + + @Override + public TypeStruct compute() { + if( _ts==null ) return TypeStruct.BOT; + Field[] fs = new Field[_ts._fields.length]; + for( int i=0; i<fs.length; i++ ) + fs[i] = _ts._fields[i].makeFrom(in(i)==null ? Type.TOP : in(i)._type); + return TypeStruct.make(_ts._name,fs); + } + + @Override + public Node idealize() { return null; } + + @Override + boolean eq(Node n) { return _ts == ((StructNode)n)._ts; } + + @Override + int hash() { return _ts==null ? 0 : _ts.hashCode(); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/SubFNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/SubFNode.java new file mode 100644 index 00000000..b591718d --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/SubFNode.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; + +import java.util.BitSet; + +public class SubFNode extends Node { + public SubFNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "SubF"; } + + @Override public String glabel() { return "-"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("-"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + if (in(1)._type instanceof TypeFloat i0 && + in(2)._type instanceof TypeFloat i1) { + if (i0.isConstant() && i1.isConstant()) + return TypeFloat.constant(i0.value()-i1.value()); + } + return in(1)._type.meet(in(2)._type); + } + + @Override + public Node idealize() { + Node lhs = in(1); + Type t2 = in(2)._type; + + // Sub of 0. + if ( t2.isConstant() && t2 instanceof TypeFloat i && i.value()==0 ) + return lhs; + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new SubFNode(lhs,rhs); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/SubNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/SubNode.java new file mode 100644 index 00000000..339c65ca --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/SubNode.java @@ -0,0 +1,59 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; + +public class SubNode extends Node { + public SubNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + + @Override public String label() { return "Sub"; } + + @Override public String glabel() { return "-"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + in(1)._print0(sb.append("("), visited); + in(2)._print0(sb.append("-"), visited); + return sb.append(")"); + } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1.isHigh() || t2.isHigh() ) + return TypeInteger.TOP; + // Sub of same is 0 + if( in(1)==in(2) ) + return TypeInteger.ZERO; + if( t1 instanceof TypeInteger i1 && + t2 instanceof TypeInteger i2 ) { + if (i1.isConstant() && i2.isConstant()) + return TypeInteger.constant(i1.value()-i2.value()); + // Fold ranges like {2-3} - {0-1} into {1-3}. + if( !AddNode.overflow(i1._min,-i2._max) && + !AddNode.overflow(i1._max,-i2._min) && + i2._min != Long.MIN_VALUE ) + return TypeInteger.make(i1._min-i2._max,i1._max-i2._min); + } + + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + // x - (-y) is x+y + if( in(2) instanceof MinusNode minus ) + return new AddNode(in(1),minus.in(1)); + + // (-x) - y is -(x+y) + if( in(1) instanceof MinusNode minus ) + return new MinusNode(new AddNode(minus.in(1),in(2)).peephole()); + + return null; + } + + @Override Node copy(Node lhs, Node rhs) { return new SubNode(lhs,rhs); } + @Override Node copyF() { return new SubFNode(null,null); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java new file mode 100644 index 00000000..1f4d67e0 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java @@ -0,0 +1,34 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.type.TypeInteger; + +import java.util.BitSet; + +public class ToFloatNode extends Node { + public ToFloatNode(Node lhs) { super(null, lhs); } + + @Override public String label() { return "ToFloat"; } + + @Override public String glabel() { return "(flt)"; } + + @Override + StringBuilder _print1(StringBuilder sb, BitSet visited) { + return in(1)._print0(sb.append("(flt)"), visited); + } + + @Override + public Type compute() { + if( in(1)._type == Type.NIL ) return TypeFloat.FZERO; + if (in(1)._type instanceof TypeInteger i0 ) { + if( i0.isHigh() ) return TypeFloat.F64.dual(); + if( i0.isConstant() ) + return TypeFloat.constant(i0.value()); + } + return TypeFloat.F64; + } + + @Override public Node idealize() { return null; } + @Override Node copy(Node lhs, Node rhs) { return new ToFloatNode(lhs); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java new file mode 100644 index 00000000..9b4245c7 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import java.util.BitSet; + +public class XCtrlNode extends CFGNode { + public XCtrlNode() { super(new Node[]{CodeGen.CODE._start}); } + @Override public String label() { return "Xctrl"; } + @Override StringBuilder _print1(StringBuilder sb, BitSet visited) { return sb.append("Xctrl"); } + @Override public boolean isConst() { return true; } + @Override public boolean isMultiTail() { return true; } + @Override public Type compute() { return Type.XCONTROL; } + @Override public Node idealize() { return null; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/XorNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/XorNode.java new file mode 100644 index 00000000..37d7147d --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/XorNode.java @@ -0,0 +1,54 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; + +public class XorNode extends LogicalNode { + public XorNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs); } + + @Override public String label() { return "Xor"; } + @Override public String op() { return "^"; } + + @Override public String glabel() { return "^"; } + + @Override + public Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1.isHigh() || t2.isHigh() ) + return TypeInteger.TOP; + if( t1 instanceof TypeInteger i0 && + t2 instanceof TypeInteger i1 ) { + if( i0.isConstant() && i1.isConstant() ) + return TypeInteger.constant(i0.value()^i1.value()); + } + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t1 = lhs._type; + Type t2 = rhs._type; + + // Xor of 0. We do not check for (0^x) because this will already + // canonicalize to (x^0) + if( t2.isConstant() && t2 instanceof TypeInteger i && i.value()==0 ) + return lhs; + + // Move constants to RHS: con*arg becomes arg*con + if ( t1.isConstant() && !t2.isConstant() ) + return swap12(); + + // Do we have ((x ^ (phi cons)) ^ con) ? + // Do we have ((x ^ (phi cons)) ^ (phi cons)) ? + // Push constant up through the phi: x ^ (phi con0^con0 con1^con1...) + Node phicon = AddNode.phiCon(this,true); + if( phicon!=null ) return phicon; + + return null; + } + @Override Node copy(Node lhs, Node rhs) { return new XorNode(_loc,lhs,rhs); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java new file mode 100644 index 00000000..4f75edcd --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.node.*; + +public class AddFMemX86 extends MemOpX86 { + AddFMemX86( AddFNode add, LoadNode ld , Node base, Node idx, int off, int scale, Node val ) { + super(add,ld, base, idx, off, scale, 0, val ); + } + + @Override public RegMask regmap(int i) { + if( i==1 ) return RegMask.EMPTY; // Memory + if( i==2 ) return x86_64_v2.RMASK; // base + if( i==3 ) return x86_64_v2.RMASK; // index + if( i==4 ) return x86_64_v2.XMASK; // value + throw Utils.TODO(); + } + + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + + + // General form: "add dst = src + [base + idx<<2 + 12]" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = "); + sb.p(val()==null ? "#"+_imm : code.reg(val())).p(" + "); + asm_address(code,sb); + } + @Override public String op() { return "addf"+_sz; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFX86.java new file mode 100644 index 00000000..a3f5c7b3 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFX86.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class AddFX86 extends MachConcreteNode implements MachNode { + AddFX86( Node addf ) { super(addf); } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "addf dst += src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" + ").p(code.reg(in(2))); + } + + @Override public String op() { return "addf"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddIX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddIX86.java new file mode 100644 index 00000000..6a332ff5 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddIX86.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class AddIX86 extends MachConcreteNode implements MachNode { + final TypeInteger _ti; + AddIX86( Node add, TypeInteger ti ) { + super(add); + _inputs.pop(); + _ti = ti; + } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { + // assert i== i; + return x86_64_v2.WMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "addi dst += #imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" + #"); + _ti.print(sb); + } + + @Override public String op() { + return _ti.value() == 1 ? "inc" : (_ti.value() == -1 ? "dec" : "addi"); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddMemX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddMemX86.java new file mode 100644 index 00000000..7d45d757 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddMemX86.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.node.*; + +public class AddMemX86 extends MemOpX86 { + AddMemX86( AddNode add, LoadNode ld , Node base, Node idx, int off, int scale, int imm, Node val ) { + super(add,ld, base, idx, off, scale, imm, val ); + } + + @Override public String op() { return "add"+_sz; } + + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + + // General form: "add dst = src + [base + idx<<2 + 12]" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = "); + sb.p(val()==null ? "#"+_imm : code.reg(val())).p(" + "); + asm_address(code,sb); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddX86.java new file mode 100644 index 00000000..61aa5a9e --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddX86.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class AddX86 extends MachConcreteNode implements MachNode { + AddX86( Node add ) { super(add); } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.RMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "add dst += src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" + ").p(code.reg(in(2))); + } + + @Override public String op() { return "add"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndIX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndIX86.java new file mode 100644 index 00000000..f2ee1c66 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndIX86.java @@ -0,0 +1,42 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; +import java.util.BitSet; +import java.lang.StringBuilder; + +public class AndIX86 extends MachConcreteNode implements MachNode { + final TypeInteger _ti; + AndIX86(Node and, TypeInteger ti) { + super(and); + _inputs.pop(); + _ti = ti; + } + + // Register mask allowed on input i. + // This is the normal calling convention + @Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.WMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form + // General form: "andi dst & #imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" & #"); + _ti.print(sb); + } + + @Override public String op() { return "andi"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallRX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallRX86.java new file mode 100644 index 00000000..31d303ed --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallRX86.java @@ -0,0 +1,31 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; +import java.io.ByteArrayOutputStream; + +public class CallRX86 extends CallNode implements MachNode { + CallRX86( CallNode call ) { super(call); } + + @Override public String label() { return op(); } + @Override public RegMask regmap(int i) { + return i==_inputs._len + ? x86_64_v2.WMASK // Function call target + : x86_64_v2.callInMask(i); // Normal argument + } + @Override public RegMask outregmap() { return x86_64_v2.RET_MASK; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(fptr())); + } + + @Override public String op() { return "callr"; } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallX86.java new file mode 100644 index 00000000..9c246c24 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallX86.java @@ -0,0 +1,40 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; +import java.io.ByteArrayOutputStream; +import java.util.BitSet; + +public class CallX86 extends CallNode implements MachNode { + final TypeFunPtr _tfp; + final String _name; + CallX86( CallNode call, TypeFunPtr tfp ) { + super(call); + _inputs.pop(); // Pop constant target + assert tfp.isConstant(); + _tfp = tfp; + _name = CodeGen.CODE.link(tfp)._name; + } + + @Override public String label() { return op(); } + @Override public RegMask regmap(int i) { + return x86_64_v2.callInMask(i); // Normal argument + } + @Override public RegMask outregmap() { return x86_64_v2.RET_MASK; } + + @Override public String name() { return _name; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(_name); + } + + @Override public String op() { return "call"; } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpIX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpIX86.java new file mode 100644 index 00000000..2fa7fbb1 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpIX86.java @@ -0,0 +1,43 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +// Compare immediate. Sets flags. +public class CmpIX86 extends MachConcreteNode implements MachNode { + final int _imm; + final String _bop; + CmpIX86( BoolNode bool, TypeInteger ti ) { + super(bool); + _inputs.pop(); // Toss ideal ConstantNode away, embedded into machine op + _bop = bool.op(); // One of <,<=,== + _imm = (int)ti.value(); + assert _imm == ti.value(); + } + CmpIX86( Node cmp, double ignore ) { + super(cmp); + _bop = "=="; + _imm = 0; + } + + @Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.FLAGS_MASK; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + @Override public void asm(CodeGen code, SB sb) { + String dst = code.reg(this); + if( dst!="FLAGS" ) sb.p(dst).p(" = "); + sb.p(code.reg(in(1))); + if( _imm != 0 ) sb.p(", #").p(_imm); + } + + @Override public String op() { return _imm==0 ? "test" : "cmp"; } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpMemX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpMemX86.java new file mode 100644 index 00000000..873ca9df --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpMemX86.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.node.*; + +public class CmpMemX86 extends MemOpX86 { + final boolean _invert; // Op switched LHS, RHS + CmpMemX86( BoolNode bool, LoadNode ld , Node base, Node idx, int off, int scale, int imm, Node val, boolean invert ) { + super(bool,ld, base, idx, off, scale, imm, val ); + _invert = invert; + } + + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.FLAGS_MASK; } + + // General form: "cmp dst = src, [base + idx<<2 + 12]" + @Override public void asm(CodeGen code, SB sb) { + String dst = code.reg(this); + if( dst!="FLAGS" ) sb.p(dst).p(" = "); + if( _invert ) asm_address(code,sb).p(","); + if( val()==null ) { + if( _imm!=0 ) sb.p("#").p(_imm); + } else { + sb.p(code.reg(val())); + } + if( !_invert ) asm_address(code,sb.p(", ")); + } + + @Override public String op() { return ((val()==null && _imm==0) ? "test" : "cmp") + _sz; } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpX86.java new file mode 100644 index 00000000..6b502249 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpX86.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class CmpX86 extends MachConcreteNode implements MachNode { + CmpX86( Node cmp ) { super(cmp); } + + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.FLAGS_MASK; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "add dst += src" + @Override public void asm(CodeGen code, SB sb) { + String dst = code.reg(this); + if( dst!="FLAGS" ) sb.p(dst).p(" = "); + sb.p(code.reg(in(1))).p(", ").p(code.reg(in(2))); + } + + @Override public String op() { return "cmp"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivFX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivFX86.java new file mode 100644 index 00000000..d73bfd5b --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivFX86.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class DivFX86 extends MachConcreteNode implements MachNode { + DivFX86( Node divf) { super(divf); } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + + // General form: "divf dst /= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" / ").p(code.reg(in(2))); + } + + @Override public String op() { + // 8 bytes = 64 bits + return "divf"; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivX86.java new file mode 100644 index 00000000..1dc20017 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivX86.java @@ -0,0 +1,31 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class DivX86 extends MachConcreteNode implements MachNode { + DivX86( Node div ) { super(div); } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { + assert i==1 || i==2; + return x86_64_v2.RMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "div dst /= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" / ").p(code.reg(in(2))); + } + + @Override public String op() { return "div"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FltX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FltX86.java new file mode 100644 index 00000000..21529f7b --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FltX86.java @@ -0,0 +1,36 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; + +import java.io.ByteArrayOutputStream; + +public class FltX86 extends ConstantNode implements MachNode { + FltX86(ConstantNode con ) { super(con); } + + // Register mask allowed on input i. 0 for no register. + @Override public RegMask regmap(int i) { return RegMask.EMPTY; } + // General int registers + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // Human-readable form appended to the SB. Things like the encoding, + // indentation, leading address or block labels not printed here. + // Just something like "ld4\tR17=[R18+12] // Load array base". + // General form: "op\tdst=src+src" + @Override public void asm(CodeGen code, SB sb) { + _con.print(sb.p(code.reg(this)).p(" #")); + } + + @Override public String op() { + return "fld"; // Some fancier encoding + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FunX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FunX86.java new file mode 100644 index 00000000..f969fae4 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FunX86.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.MachNode; +import java.io.ByteArrayOutputStream; + +public class FunX86 extends FunNode implements MachNode { + FunX86( FunNode fun ) { super(fun); } + + // Register mask allowed on input i. 0 for no register. + @Override public RegMask regmap(int i) { return RegMask.EMPTY; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return RegMask.EMPTY; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/I2f8X86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/I2f8X86.java new file mode 100644 index 00000000..983f817d --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/I2f8X86.java @@ -0,0 +1,28 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class I2f8X86 extends MachConcreteNode implements MachNode { + I2f8X86(Node i2f8 ) { super(i2f8); } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.WMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "i2f8 (flt)int_value" + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p("(flt)").p(code.reg(in(1))); + } + + @Override public String op() { return "i2f8"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/IntX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/IntX86.java new file mode 100644 index 00000000..2cd27c14 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/IntX86.java @@ -0,0 +1,45 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +import java.io.ByteArrayOutputStream; + +// Integer constants +public class IntX86 extends ConstantNode implements MachNode { + + IntX86( ConstantNode con ) { + super(con); + } + + // Register mask allowed on input i. 0 for no register. + @Override public RegMask regmap(int i) { return RegMask.EMPTY; } + // General int registers + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // Human-readable form appended to the SB. Things like the encoding, + // indentation, leading address or block labels not printed here. + // Just something like "ld4\tR17=[R18+12] // Load array base". + // General form: "op\tdst=src+src" + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + if( _con == Type.NIL || _con == TypeInteger.ZERO ) + sb.p(reg).p(",").p(reg); + else + _con.print(sb.p(reg).p(" #")); + } + + @Override public String op() { + if( _con == Type.NIL || _con == TypeInteger.ZERO ) + return "xor"; + return "ldi"; // Some fancier encoding + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java new file mode 100644 index 00000000..7cf4444c --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java @@ -0,0 +1,46 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +// Jump on flags, uses flags +public class JmpX86 extends IfNode implements MachNode { + final String _bop; + JmpX86( IfNode iff, String bop ) { + super(iff); + _bop = bop; + } + + @Override public String label() { return op(); } + + @Override public void postSelect() { + Node set = in(1); + Node cmp = set.in(1); + // Bypass an expected Set and just reference the cmp directly + if( set instanceof SetX86 && (cmp instanceof CmpX86 || cmp instanceof CmpIX86 || cmp instanceof CmpMemX86) ) + _inputs.set(1,cmp); + else + throw Utils.TODO(); + } + @Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.FLAGS_MASK; } + @Override public RegMask outregmap() { return RegMask.EMPTY; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + @Override public void asm(CodeGen code, SB sb) { + String src = code.reg(in(1)); + if( src!="FLAGS" ) sb.p(src); + } + + @Override public String op() { return "j"+_bop; } + + @Override public String comment() { + return "L"+cproj(1)._nid+", L"+cproj(0)._nid; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LeaX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LeaX86.java new file mode 100644 index 00000000..a49b8d7f --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LeaX86.java @@ -0,0 +1,42 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class LeaX86 extends MachConcreteNode implements MachNode { + final int _scale; + final long _offset; + LeaX86( Node add, Node base, Node idx, int scale, long offset ) { + super(add); + assert scale==1 || scale==2 || scale==4 || scale==8; + _inputs.pop(); + _inputs.pop(); + _inputs.push(base); + _inputs.push(idx); + _scale = scale; + _offset = offset; + } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.RMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "lea dst = base + 4*idx + 12" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = "); + if( in(1) != null ) + sb.p(code.reg(in(1))).p(" + "); + sb.p(code.reg(in(2))).p("*").p(_scale); + if( _offset!=0 ) sb.p(" + #").p(_offset); + } + + @Override public String op() { return "lea"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java new file mode 100644 index 00000000..02a9a4fe --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.node.LoadNode; +import com.seaofnodes.simple.node.Node; + +public class LoadX86 extends MemOpX86 { + LoadX86( LoadNode ld, Node base, Node idx, int off, int scale ) { + super(ld,ld, base, idx, off, scale, 0); + } + + @Override public String op() { return "ld"+_sz; } + + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + + // General form: "ldN dst,[base + idx<<2 + 12]" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(","); + asm_address(code,sb); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemAddX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemAddX86.java new file mode 100644 index 00000000..418f63f3 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemAddX86.java @@ -0,0 +1,28 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.node.*; + +public class MemAddX86 extends MemOpX86 { + MemAddX86( StoreNode st, Node base, Node idx, int off, int scale, int imm, Node val ) { + super(st, st, base, idx, off, scale, imm, val ); + } + + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return RegMask.EMPTY; } + + + // General form: "add [base + idx<<2 + 12] += src" + @Override public void asm(CodeGen code, SB sb) { + asm_address(code,sb); + if( val()==null ) { + if( _imm != 1 && _imm != -1 ) sb.p(" += #").p(_imm); + } else sb.p(" += ").p(code.reg(val())); + } + + @Override public String op() { + return (_imm == 1 ? "inc" : (_imm == -1 ? "dec" : "add")) + _sz; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java new file mode 100644 index 00000000..1e678a0b --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java @@ -0,0 +1,87 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import java.io.ByteArrayOutputStream; +import java.lang.StringBuilder; +import java.util.BitSet; + +// Generic X86 memory operand base. +// inputs: +// ctrl - will appear for possibly RCE (i.e. array ops) +// mem - memory dependence edge +// base - Basic object pointer +// idx/null - Scaled index offset, or null if none +// val/null - Value to store, as part of a op-to-mem or op-from-mem. Null for loads, or if an immediate is being used +// Constants: +// offset - offset added to base. Can be zero. +// scale - scale on index; only 0,1,2,4,8 allowed, 0 is only when index is null +// imm - immediate value to store or op-to-mem, and only when val is null +public abstract class MemOpX86 extends MemOpNode implements MachNode { + final int _off; // Limit 32 bits + final int _scale; // Limit 0,1,2,3 + final int _imm; // Limit 32 bits + final char _sz = (char)('0'+(1<<_declaredType.log_size())); + MemOpX86( Node op, MemOpNode mop, Node base, Node idx, int off, int scale, int imm ) { + super(op,mop); + assert base._type instanceof TypeMemPtr && !(base instanceof AddNode); + assert (idx==null && scale==0) || (idx!=null && 0<= scale && scale<=3); + + // Copy memory parts from eg the LoadNode over the opcode, e.g. an Add + if( op != mop ) { + _inputs.set(0,mop.in(0)); // Control from mem op + _inputs.set(1,mop.in(1)); // Memory from mem op + _inputs.set(2,base); // Base handed in + } + + assert ptr() == base; + _inputs.setX(3,idx); + _off = off; + _scale = scale; + _imm = imm; + } + + // Store-based flavors have a value edge + MemOpX86( Node op, MemOpNode mop, Node base, Node idx, int off, int scale, int imm, Node val ) { + this(op,mop,base,idx,off,scale,imm); + _inputs.setX(4,val); + } + + Node idx() { return in(3); } + Node val() { return in(4); } + + @Override public StringBuilder _printMach(StringBuilder sb, BitSet visited) { return sb.append(".").append(_name); } + + @Override public String label() { return op(); } + @Override public Type compute() { throw Utils.TODO(); } + @Override public Node idealize() { throw Utils.TODO(); } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { + if( i==1 ) return RegMask.EMPTY; // Memory + if( i==2 ) return x86_64_v2.RMASK; // base + if( i==3 ) return x86_64_v2.RMASK; // index + if( i==4 ) return x86_64_v2.RMASK; // value + throw Utils.TODO(); + } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { throw Utils.TODO(); } + + @Override public int encoding(ByteArrayOutputStream bytes) { throw Utils.TODO(); } + + + // "[base + idx<<2 + 12]" + SB asm_address(CodeGen code, SB sb) { + sb.p("[").p(code.reg(ptr())); + if( idx() != null ) { + sb.p("+").p(code.reg(idx())); + if( _scale != 0 ) + sb.p("<<").p(_scale); + } + if( _off != 0 ) + sb.p("+").p(_off); + return sb.p("]"); + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulFX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulFX86.java new file mode 100644 index 00000000..aef813e1 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulFX86.java @@ -0,0 +1,30 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class MulFX86 extends MachConcreteNode implements MachNode { + MulFX86( Node mulf) { super(mulf); } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "mulf dst *= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" * ").p(code.reg(in(2))); + } + + @Override public String op() { return "mulf"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulIX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulIX86.java new file mode 100644 index 00000000..7508cf4c --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulIX86.java @@ -0,0 +1,36 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class MulIX86 extends MachConcreteNode implements MachNode { + final TypeInteger _ti; + MulIX86(Node mul, TypeInteger ti) {super(mul); _inputs.pop(); _ti = ti;} + + // Register mask allowed on input i. + // This is the normal calling convention + @Override public RegMask regmap(int i) { + // assert i==1; + return x86_64_v2.WMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + // General form + // General form: "muli dst * #imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" * #"); + _ti.print(sb); + } + + @Override public String op() { return "muli"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulX86.java new file mode 100644 index 00000000..15fd9496 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulX86.java @@ -0,0 +1,30 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class MulX86 extends MachConcreteNode implements MachNode { + + MulX86( Node mul ) { super(mul); } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.RMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "mul dst *= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" * ").p(code.reg(in(2))); + } + + @Override public String op() { return "mul"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java new file mode 100644 index 00000000..4396a917 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java @@ -0,0 +1,40 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import java.io.ByteArrayOutputStream; + +public class NewX86 extends NewNode implements MachNode { + + // A pre-zeroed chunk of memory. + NewX86( NewNode nnn ) { super(nnn); } + + // Register mask allowed on input i, the size + @Override public RegMask regmap(int i) { + // Size + if( i==1 ) return x86_64_v2.RDI_MASK; + // All the memory alias edges + return RegMask.EMPTY; + } + @Override public RegMask outregmap() { throw Utils.TODO(); } + + // Register mask allowed as a result. Pointer result in standard calling + // convention. + @Override public RegMask outregmap(int i) { + if( i == 1 ) return x86_64_v2.RET_MASK; + // All the memory aliases edges + return RegMask.EMPTY; + } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "alloc #bytes" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(size())); + } + + @Override public String op() { return "alloc"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrIX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrIX86.java new file mode 100644 index 00000000..513962c2 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrIX86.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeInteger; + +import java.io.ByteArrayOutputStream; + +public class OrIX86 extends MachConcreteNode implements MachNode { + + final TypeInteger _ti; + OrIX86(Node or, TypeInteger ti) {super(or); _inputs.pop(); _ti = ti;} + + // Register mask allowed on input i. + // This is the normal calling convention + @Override public RegMask regmap(int i) { + //assert i==1; + return x86_64_v2.WMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form + // General form: "ori ori | #imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" | #"); + _ti.print(sb); + } + + @Override public String op() { return "ori"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ParmX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ParmX86.java new file mode 100644 index 00000000..8a439644 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ParmX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.ParmNode; +import com.seaofnodes.simple.node.MachNode; +import java.io.ByteArrayOutputStream; + +public class ParmX86 extends ParmNode implements MachNode { + final RegMask _rmask; + ParmX86( ParmNode parm ) { + super(parm); + _rmask = x86_64_v2.callInMask(_idx); + } + + // Register mask allowed on input i. 0 for no register. + @Override public RegMask regmap(int i) { return RegMask.EMPTY; } + // Register mask allowed as a result. Calling convention register + @Override public RegMask outregmap() { return _rmask; } + + // Encoding is appended into the byte array. Returns size + @Override public int encoding(ByteArrayOutputStream bytes) { return 0; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ProjX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ProjX86.java new file mode 100644 index 00000000..2849b232 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ProjX86.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.ProjNode; +import com.seaofnodes.simple.node.MachNode; +import java.io.ByteArrayOutputStream; + +public class ProjX86 extends ProjNode implements MachNode { + + ProjX86( ProjNode p ) { super(p); } + + // Register mask allowed on input i. 0 for no register. + @Override public RegMask regmap(int i) { return RegMask.EMPTY; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return RegMask.EMPTY; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RetX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RetX86.java new file mode 100644 index 00000000..e6219ff4 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RetX86.java @@ -0,0 +1,48 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.ReturnNode; +import com.seaofnodes.simple.node.MachNode; +import java.io.ByteArrayOutputStream; + +// Return +public class RetX86 extends ReturnNode implements MachNode { + RetX86( ReturnNode ret, FunNode fun ) { super(ret, fun); fun.setRet(this); } + + // Correct Nodes outside the normal edges + @Override public void postSelect() { + FunNode fun = (FunNode)rpc().in(0); + _fun = fun; + fun.setRet(this); + } + + // Register mask allowed on input i. + // This is the normal calling convention + @Override public RegMask regmap(int i) { + return switch( i ) { + case 0 -> RegMask.EMPTY; + case 1 -> RegMask.EMPTY; + case 2 -> x86_64_v2.RET_MASK; + case 3 -> RegMask.EMPTY; // RPC is always on stack + default -> throw Utils.TODO(); + }; + } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return RegMask.EMPTY; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // Human-readable form appended to the SB. Things like the encoding, + // indentation, leading address or block labels not printed here. + // Just something like "ld4\tR17=[R18+12] // Load array base". + // General form: "op\tdst=src+src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(in(2))); + } + + @Override public String op() { return "ret"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarX86.java new file mode 100644 index 00000000..3d907eef --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarX86.java @@ -0,0 +1,40 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; +import java.util.BitSet; +import java.lang.StringBuilder; + + +public class SarX86 extends MachConcreteNode implements MachNode { + final TypeInteger _ti; + SarX86(Node sar, TypeInteger ti) {super(sar); _inputs.pop(); _ti = ti;} + + // Register mask allowed on input i. + // This is the normal calling convention + @Override public RegMask regmap(int i) { + // assert i==1; + return x86_64_v2.RMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form + // General form: "sari dst << #imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" >> #"); + _ti.print(sb); + } + + @Override public String op() { return "sari"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java new file mode 100644 index 00000000..25367e03 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +// Compare immediate. Sets flags. +// Corresponds to the x86 instruction "sete && setne". +// Use result of comparison without jump. +public class SetX86 extends MachConcreteNode implements MachNode { + final String _bop; // One of <,<=,== + // Constructor expects input is an X86 and not an Ideal node. + SetX86( Node cmp, String bop ) { + super(cmp); + _inputs.setLen(1); // Pop the cmp inputs + // Replace with the matched cmp + _inputs.push(cmp); + _bop = bop; + } + + @Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.FLAGS_MASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)); + String src = code.reg(in(1)); + if( src!="FLAGS" ) sb.p(" = ").p(src); + } + + @Override public String op() { return "set"+_bop; } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlIX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlIX86.java new file mode 100644 index 00000000..61e0d262 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlIX86.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; +import java.util.BitSet; + +public class ShlIX86 extends MachConcreteNode implements MachNode{ + final TypeInteger _ti; + ShlIX86(Node shl, TypeInteger ti) {super(shl); _inputs.pop(); _ti = ti;} + + // Register mask allowed on input i. + // This is the normal calling convention + @Override public RegMask regmap(int i) { + //assert i==1; + return x86_64_v2.RMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form + // General form: "shli dst << #imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" << #"); + _ti.print(sb); + } + + @Override public String op() { return "shli"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrIX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrIX86.java new file mode 100644 index 00000000..08cae93e --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrIX86.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; +import java.util.BitSet; +import java.lang.StringBuilder; + +public class ShrIX86 extends MachConcreteNode implements MachNode{ + final TypeInteger _ti; + ShrIX86(Node shr, TypeInteger ti) {super(shr); _inputs.pop(); _ti = ti;} + + // Register mask allowed on input i. + // This is the normal calling convention + @Override public RegMask regmap(int i) { + //assert i==1; + return x86_64_v2.RMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form + // General form: "shri dst >>> #imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" >>> #"); + _ti.print(sb); + } + + @Override public String op() { return "shri"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java new file mode 100644 index 00000000..c9191b87 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java @@ -0,0 +1,30 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StoreNode; +import java.util.BitSet; + +public class StoreX86 extends MemOpX86 { + StoreX86( StoreNode st, Node base, Node idx, int off, int scale, int imm, Node val ) { + super(st,st, base, idx, off, scale, imm, val); + } + + @Override public String op() { return "st"+_sz; } + + @Override public StringBuilder _printMach(StringBuilder sb, BitSet visited) { + return sb.append(".").append(_name).append("=").append(val()==null ? _imm : val()).append(";"); + } + + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return RegMask.EMPTY; } + + // General form: "stN [base + idx<<2 + 12],val" + @Override public void asm(CodeGen code, SB sb) { + asm_address(code,sb).p(","); + if( val()==null ) sb.p("#").p(_imm); + else sb.p(code.reg(val())); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubFX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubFX86.java new file mode 100644 index 00000000..ed19ad79 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubFX86.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import java.io.ByteArrayOutputStream; + +public class SubFX86 extends MachConcreteNode implements MachNode { + SubFX86( Node subf) { super(subf); } + + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "subf dst -= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" - ").p(code.reg(in(2))); + } + + @Override public String op() { return "subf"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubX86.java new file mode 100644 index 00000000..cac59d3b --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubX86.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.ByteArrayOutputStream; + +public class SubX86 extends MachConcreteNode implements MachNode { + SubX86( Node sub ) { super(sub); } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.RMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form: "sub dst -= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" - ").p(code.reg(in(2))); + } + + @Override public String op() { return "sub"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java new file mode 100644 index 00000000..bd88011f --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java @@ -0,0 +1,41 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; + +import java.io.ByteArrayOutputStream; + +// Function constants +public class TFPX86 extends ConstantNode implements MachNode { + TFPX86( ConstantNode con ) { super(con); } + // Register mask allowed on input i. 0 for no register. + @Override public RegMask regmap(int i) { return RegMask.EMPTY; } + // General int registers + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // Human-readable form appended to the SB. Things like the encoding, + // indentation, leading address or block labels not printed here. + // Just something like "ld4\tR17=[R18+12] // Load array base". + // General form: "op\tdst=src+src" + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + if( _con == Type.NIL ) + sb.p(reg).p(",").p(reg); + else + _con.print(sb.p(reg).p(" #")); + } + + @Override public String op() { + if( _con == Type.NIL ) + return "xor"; + return "ldx"; // Some fancier encoding + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorIX86.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorIX86.java new file mode 100644 index 00000000..795fa814 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorIX86.java @@ -0,0 +1,42 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeInteger; + +import java.io.ByteArrayOutputStream; + +public class XorIX86 extends MachConcreteNode implements MachNode { + + final TypeInteger _ti; + XorIX86(Node xor, TypeInteger ti) {super(xor); _inputs.pop(); _ti = ti;} + + // Register mask allowed on input i. + // This is the normal calling convention + @Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.WMASK; } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + + // Output is same register as input#1 + @Override public int twoAddress() { return 1; } + + // Encoding is appended into the byte array; size is returned + @Override public int encoding(ByteArrayOutputStream bytes) { + throw Utils.TODO(); + } + + // General form + // General form: "ori xori ^ #imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" ^ #"); + _ti.print(sb); + } + + @Override public String op() { return "xori"; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java new file mode 100644 index 00000000..45ae19dc --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java @@ -0,0 +1,418 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.Machine; +import com.seaofnodes.simple.RegMask; +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + +public class x86_64_v2 extends Machine { + // X86-64 V2. Includes e.g. SSE4.2 and POPCNT. + @Override public String name() { return "x86_64_v2"; } + + public static int RAX = 0, RCX = 1, RDX = 2, RBX = 3, RSP = 4, RBP = 5, RSI = 6, RDI = 7; + public static int R08 = 8, R09 = 9, R10 = 10, R11 = 11, R12 = 12, R13 = 13, R14 = 14, R15 = 15; + public static int FLAGS = 32; + + public static int XMM0 = 16, XMM1 = 17, XMM2 = 18, XMM3 = 19, XMM4 = 20, XMM5 = 21, XMM6 = 22, XMM7 = 23; + public static int XMM8 = 24, XMM9 = 25, XMM10 = 26, XMM11 = 27, XMM12 = 28, XMM13 = 29, XMM14 = 30, XMM15 = 31; + + // General purpose register mask: pointers and ints, not floats + public static RegMask RMASK = new RegMask(0b1111111111111111); + // No RSP in the *write* general set. + public static RegMask WMASK = new RegMask(0b1111111111101111); + // Xmm register mask + public static RegMask XMASK = new RegMask( 0b1111111111111111L << XMM0); + + public static RegMask FLAGS_MASK = new RegMask(1L<<FLAGS); + + // Return single int/ptr register + public static RegMask RET_MASK = new RegMask(RAX); + + public static RegMask RDI_MASK = new RegMask(1L<<RDI); + public static RegMask RCX_MASK = new RegMask(1L<<RCX); + public static RegMask RDX_MASK = new RegMask(1L<<RDX); + public static RegMask R08_MASK = new RegMask(1L<<R08); + public static RegMask R09_MASK = new RegMask(1L<<R09); + public static RegMask RSI_MASK = new RegMask(1L<<RSI); + + // Calling conv metadata + public int GPR_COUNT_CONV_WIN64 = 4; // RCX, RDX, R9, R9 + public int XMM_COUNT_CONV_WIN64 = 4; // XMM0L, XMM1L, XMM2L, XMM3L + + public int GPR_COUNT_CONV_SYSTEM_V = 6; // RDI, RSI, RDX, RCX, R8, R9 + public int XMM_COUNT_CONV_SYSTEM_V = 4; // XMM0, XMM1, XMM2, XMM3 .... + // Human-readable name for a register number, e.g. "RAX". + // Hard crash for bad register number, fix yer bugs! + public static final String[] REGS = new String[] { + "rax" , "rcx" , "rdx" , "rbx" , "rsp" , "rbp" , "rsi" , "rdi" , + "r8" , "r9" , "r10" , "r11" , "r12" , "r13" , "r14" , "r15" , + "xmm0", "xmm1", "xmm2" , "xmm3" , "xmm4" , "xmm5" , "xmm6" , "xmm7" , + "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15", + }; + @Override public String reg( int reg ) { return REGS[reg]; } + + // Calling convention; returns a machine-specific register + // for incoming argument idx. + // index 0 for control, 1 for memory, real args start at index 2 + static int callInArg( int idx ) { + return switch( CodeGen.CODE._callingConv ) { + case CodeGen.CallingConv.SystemV -> callInArgSystemV(idx); + case CodeGen.CallingConv.Win64 -> callInArgWin64 (idx); + }; + } + + // WIN64(param passing) + static RegMask[] CALLINMASK_WIN64 = new RegMask[] { + RegMask.EMPTY, + RegMask.EMPTY, + RCX_MASK, + RDX_MASK, + R08_MASK, + R09_MASK, + RegMask.EMPTY, + RegMask.EMPTY + }; + static int[] CALLINARG_WIN64 = new int[] { + 0, // Control, no register + 0, // Memory, no register + RCX, // + RDX, + R08, + R09, + 0, + 0, + }; + static int callInArgWin64( int idx ) { return CALLINARG_WIN64[idx]; } + + // caller saved(win64) + public static final long WIN64_ABI_CALLER_SAVED = + (1L << RAX) | (1L << RCX) | (1L << RDX) | (1L << R08) | (1L << R09) | (1L << R10) | (1L << R11); + + // callee saved(win64) + public static final long WIN64_ABI_CALLEE_SAVED = ~WIN64_ABI_CALLER_SAVED; + + // SystemV(param passing) + static RegMask[] CALLINMASK_SYSTEMV = new RegMask[] { + RegMask.EMPTY, + RegMask.EMPTY, + RDI_MASK, + RSI_MASK, + RDX_MASK, + RCX_MASK, + R08_MASK, + R09_MASK + }; + static int[] CALLINARG_SYSTEMV = new int[] { + 0, // Control, no register + 0, // Memory, no register + RDI, + RSI, + RDX, + RCX, + R08, + R09, + }; + static int callInArgSystemV( int idx ) { return CALLINARG_SYSTEMV[idx]; } + + // caller saved(systemv) + // caller saved(win64) + public static final long SYSTEMV_ABI_CALLER_SAVED = + (1L << RAX) | (1L << RDI) | (1L << RSI) | (1L << RCX) | (1L << RDX) | (1L << R08) | (1L << R09) << (1L << R10) << (1L << R11); + // callee saved(systemv) + public static final long SYSTEMV_ABI_CALLE_SAVED = ~SYSTEMV_ABI_CALLER_SAVED; + + + static RegMask callInMask( int idx ) { + return switch( CodeGen.CODE._callingConv ) { + case CodeGen.CallingConv.SystemV -> CALLINMASK_SYSTEMV[idx]; + case CodeGen.CallingConv.Win64 -> CALLINMASK_WIN64 [idx]; + }; + } + + // Create a split op; any register to any register, including stack slots + @Override public Node split() { + throw Utils.TODO(); + } + + // Return a MachNode unconditional branch + @Override public CFGNode jump() { + throw Utils.TODO(); + } + + // Break an infinite loop + @Override public IfNode never( CFGNode ctrl ) { + throw Utils.TODO(); + } + + // Instruction selection + @Override public Node instSelect( Node n ) { + return switch( n ) { + case AddFNode addf -> addf(addf); + case AddNode add -> add(add); + case AndNode and -> and(and); + case BoolNode bool -> cmp(bool); + case CallEndNode cend -> new CallEndNode((CallNode)cend.in(0)); + case CallNode call -> call(call); + case CProjNode c -> new CProjNode(c); + case ConstantNode con -> con(con); + case DivFNode divf -> new DivFX86(divf); + case DivNode div -> new DivX86(div); + case FunNode fun -> new FunX86(fun); + case IfNode iff -> jmp(iff); + case LoadNode ld -> ld(ld); + case MemMergeNode mem -> new MemMergeNode(mem); + case MulFNode mulf -> new MulFX86(mulf); + case MulNode mul -> mul(mul); + case NewNode nnn -> new NewX86(nnn); + case OrNode or -> or(or); + case ParmNode parm -> new ParmX86(parm); + case PhiNode phi -> new PhiNode(phi); + case ProjNode prj -> prj(prj); + case ReadOnlyNode read -> new ReadOnlyNode(read); + case ReturnNode ret -> new RetX86(ret,ret.fun()); + case SarNode sar -> sar(sar); + case ShlNode shl -> shl(shl); + case ShrNode shr -> shr(shr); + case StartNode start -> new StartNode(start); + case StopNode stop -> new StopNode(stop); + case StoreNode st -> st(st); + case SubFNode subf -> new SubFX86(subf); + case SubNode sub -> sub(sub); + case ToFloatNode tfn -> i2f8(tfn); + case XorNode xor -> xor(xor); + + case LoopNode loop -> new LoopNode(loop); + case RegionNode region-> new RegionNode(region); + default -> throw Utils.TODO(); + }; + } + + + // Attempt a full LEA-style break down. + private Node add( AddNode add ) { + Node lhs = add.in(1); + Node rhs = add.in(2); + if( lhs instanceof LoadNode ld && ld.nOuts()==1 ) + return new AddMemX86(add,address(ld),ld.ptr(),idx,off,scale, imm(rhs),val); + + if( rhs instanceof LoadNode ld && ld.nOuts()==1 ) + throw Utils.TODO(); // Swap load sides + + // Attempt a full LEA-style break down. + // Returns one of AddX86, AddIX86, LeaX86, or LHS + if( rhs instanceof ConstantNode off && off._con instanceof TypeInteger toff ) { + if( lhs instanceof AddNode ladd ) + // ((base + (idx << scale)) + off) + return _lea(add,ladd.in(1),ladd.in(2),toff.value()); + if( lhs instanceof ShlNode shift ) + // (idx << scale) + off; no base + return _lea(add,null,shift,toff.value()); + + // lhs + rhs1 + if( toff.value()==0 ) return add; + return new AddIX86(add, toff); + } + return _lea(add,lhs,rhs,0); + } + + + private Node addf( AddFNode addf ) { + if( addf.in(1) instanceof LoadNode ld && ld.nOuts()==1 ) + return new AddFMemX86(addf,address(ld),ld.ptr(),idx,off,scale, addf.in(2)); + + if( addf.in(2) instanceof LoadNode ld && ld.nOuts()==1 ) + throw Utils.TODO(); // Swap load sides + + return new AddFX86(addf); + } + + + private Node _lea( Node add, Node base, Node idx, long off ) { + int scale = 1; + if( base instanceof ShlNode && !(idx instanceof ShlNode) ) throw Utils.TODO(); // Bug in canonicalization, should on RHS + if( idx instanceof ShlNode shift && shift.in(2) instanceof ConstantNode shfcon && + shfcon._con instanceof TypeInteger tscale && 0 <= tscale.value() && tscale.value() <= 3 ) { + idx = shift.in(1); + scale = 1 << ((int)tscale.value()); + } + // (base + idx) + off + return off==0 && scale==1 + ? new AddX86(add) + : new LeaX86(add,base,idx,scale,off); + } + + + private Node and(AndNode and) { + if( and.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti ) + return new AndIX86(and, ti); + throw Utils.TODO(); + } + + private Node call(CallNode call) { + if( call.fptr() instanceof ConstantNode con && con._con instanceof TypeFunPtr tfp ) + return new CallX86(call, tfp); + return new CallRX86( call ); + } + + // Because X86 flags, a normal ideal Bool is 2 X86 ops: a "cmp" and at "setz". + // Ideal If reading from a setz will skip it and use the "cmp" instead. + private Node cmp( BoolNode bool ) { + Node cmp = _cmp(bool); + return new SetX86(cmp,bool.op()); + } + private Node _cmp( BoolNode bool ) { + Node lhs = bool.in(1); + Node rhs = bool.in(2); + if( lhs instanceof LoadNode ld && ld.nOuts()==1 ) + return new CmpMemX86(bool,address(ld),ld.ptr(),idx,off,scale, imm(rhs),val,false); + + if( rhs instanceof LoadNode ld && ld.nOuts()==1 ) + return new CmpMemX86(bool,address(ld),ld.ptr(),idx,off,scale, imm(lhs),val,true); + + return rhs instanceof ConstantNode con && con._con instanceof TypeInteger ti + ? new CmpIX86(bool, ti) + : new CmpX86(bool); + } + + private Node con( ConstantNode con ) { + return switch( con._con ) { + case TypeInteger ti -> new IntX86(con); + case TypeFloat tf -> new FltX86(con); + case TypeFunPtr tfp -> new TFPX86(con); + case TypeMemPtr tmp -> new ConstantNode(con); + case TypeNil tn -> throw Utils.TODO(); + // TOP, BOTTOM, XCtrl, Ctrl, etc. Never any executable code. + case Type t -> new ConstantNode(con); + }; + } + + private Node i2f8(ToFloatNode tfn) { + assert tfn.in(1)._type instanceof TypeInteger ti; + return new I2f8X86(tfn); + } + + private Node jmp( IfNode iff ) { + // If/Bool combos will match to a Cmp/Set which sets flags. + // Most general arith ops will also set flags, which the Jmp needs directly. + // Loads do not set the flags, and will need an explicit TEST + if( !(iff.in(1) instanceof BoolNode) ) + iff.setDef(1,new BoolNode.EQ(iff.in(1),new ConstantNode(TypeInteger.ZERO))); + return new JmpX86(iff, ((BoolNode)iff.in(1)).op()); + } + + private Node ld( LoadNode ld ) { + return new LoadX86(address(ld),ld.ptr(),idx,off,scale); + } + + private Node mul(MulNode mul) { + return mul.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti + ? new MulIX86(mul, ti) + : new MulX86(mul); + } + + private Node or(OrNode or) { + if( or.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti) + return new OrIX86(or, ti); + throw Utils.TODO(); + } + + private Node prj( ProjNode prj ) { + return new ProjX86(prj); + } + + private Node sar( SarNode sar ) { + if( sar.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti) + return new SarX86(sar, ti); + throw Utils.TODO(); + } + + private Node shl( ShlNode shl ) { + if( shl.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti ) + return new ShlIX86(shl, ti); + throw Utils.TODO(); + } + + private Node shr(ShrNode shr) { + if( shr.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti ) + return new ShrIX86(shr, ti); + throw Utils.TODO(); + } + + private Node st( StoreNode st ) { + // Look for "*ptr op= val" + Node op = st.val(); + if( op instanceof AddNode ) { + if( op.in(1) instanceof LoadNode ld && + ld.in(0)==st.in(0) && + ld.mem()==st.mem() && + ld.ptr()==st.ptr() && + ld.off()==st.off() ) { + if( op instanceof AddNode ) { + return new MemAddX86(address(st),st.ptr(),idx,off,scale,imm(op.in(2)),val); + } + throw Utils.TODO(); + } + } + + return new StoreX86(address(st),st.ptr(),idx,off,scale,imm(st.val()),val); + } + + private Node sub( SubNode sub ) { + return sub.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti + ? new AddIX86(sub, TypeInteger.constant(-ti.value())) + : new SubX86(sub); + } + + private Node xor(XorNode xor) { + if(xor.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti) + return new XorIX86(xor, ti); + throw Utils.TODO(); + } + + + // Gather X86 addressing mode bits prior to constructing. This is a + // builder pattern, but saving the bits in a *local* *global* here to keep + // mess contained. + private static int off, scale, imm; + private static Node idx, val; + private <N extends MemOpNode> N address( N mop ) { + off = scale = imm = 0; // Reset + idx = val = null; + Node base = mop.ptr(); + // Skip/throw-away a ReadOnly, only used to typecheck + if( base instanceof ReadOnlyNode read ) base = read.in(1); + assert !(base instanceof AddNode) && base._type instanceof TypeMemPtr; // Base ptr always, not some derived + if( mop.off() instanceof AddNode add && add.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti ) { + off = (int)ti.value(); + assert off == ti.value(); // In 32-bit range + idx = add.in(1); + if( idx instanceof ShlNode shift && shift.in(2) instanceof ConstantNode shfcon && + shfcon._con instanceof TypeInteger tscale && 0 <= tscale.value() && tscale.value() <= 3 ) { + idx = shift.in(1); + scale = (int)tscale.value(); + } + } else { + if( mop.off() instanceof ConstantNode con && con._con instanceof TypeInteger ti ) { + off = (int)ti.value(); + assert off == ti.value(); // In 32-bit range + } else { + idx = mop.off(); + } + } + return mop; + } + private int imm( Node xval ) { + assert val==null && imm==0; + if( xval instanceof ConstantNode con && con._con instanceof TypeInteger ti ) { + val = null; + imm = (int)ti.value(); + assert imm == ti.value(); // In 32-bit range + } else { + val = xval; + } + return imm; + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/Field.java b/chapter20/src/main/java/com/seaofnodes/simple/type/Field.java new file mode 100644 index 00000000..ffa9d2a6 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/Field.java @@ -0,0 +1,74 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Represents a field in a struct. This is not a Type in the type system. + */ +public class Field extends Type { + + // The pair {fieldName,type} uniquely identifies a field. + + // Field name + public final String _fname; + // Type of the field + public final Type _type; + // Unique memory alias, not sensibly part of a "type" but very convenient here. + public final int _alias; + // Field must be written to exactly once, no more, no less + public final boolean _final; + + private Field(String fname, Type type, int alias, boolean xfinal ) { + super(TFLD); + _fname = fname; + _type = type; + _alias = alias; + _final = xfinal; + } + // Make with existing alias + public static Field make( String fname, Type type, int alias, boolean xfinal ) { + return new Field(fname,type,alias,xfinal).intern(); + } + public Field makeFrom( Type type ) { + return type == _type ? this : new Field(_fname,type,_alias,_final).intern(); + } + @Override public Field makeRO() { return _final ? this : make(_fname,_type.makeRO(),_alias,true); } + @Override public boolean isFinal() { return _final && _type.isFinal(); } + + public static final Field TEST = make("test",Type.NIL,-2,false); + public static final Field TEST2= make("test",Type.NIL,-2,true); + public static void gather(ArrayList<Type> ts) { ts.add(TEST); ts.add(TEST2); } + + @Override Field xmeet( Type that ) { + Field fld = (Field)that; // Invariant + assert _fname.equals(fld._fname); + assert _alias==fld._alias; + return make(_fname,_type.meet(fld._type),_alias,_final | fld._final); + } + + @Override + public Field dual() { return make(_fname,_type.dual(),_alias,!_final); } + + @Override public Field glb() { + Type glb = _type.glb(); + return (glb==_type && _final) ? this : make(_fname,glb,_alias,true); + } + + // Override in subclasses + int hash() { return _fname.hashCode() ^ _type.hashCode() ^ _alias ^ (_final ? 1024 : 0); } + + boolean eq(Type t) { + Field f = (Field)t; + return _fname.equals(f._fname) && _type==f._type && _alias==f._alias && _final==f._final; + } + + + @Override + public SB print( SB sb ) { + return _type.print(sb.p(_final?"":"!").p(_fname).p(":").p(_alias).p(" : ")); + } + + @Override public String str() { return _fname; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/Type.java b/chapter20/src/main/java/com/seaofnodes/simple/type/Type.java new file mode 100644 index 00000000..847235dd --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/Type.java @@ -0,0 +1,235 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * These types are part of a Monotone Analysis Framework, + * @see <a href="https://www.cse.psu.edu/~gxt29/teaching/cse597s21/slides/08monotoneFramework.pdf">see for example this set of slides</a>. + * <p> + * The types form a lattice; @see <a href="https://en.wikipedia.org/wiki/Lattice_(order)">a symmetric complete bounded (ranked) lattice.</a> + * <p> + * This wild lattice theory will be needed later to allow us to easily beef up + * the analysis and optimization of the Simple compiler... but we don't need it + * now, just know that it is coming along in a later Chapter. + * <p>g + * One of the fun things here is that while the theory is deep and subtle, the + * actual implementation is darn near trivial and is generally really obvious + * what we're doing with it. Right now, it's just simple integer math to do + * simple constant folding e.g. 1+2 == 3 stuff. + */ + +public class Type { + static final HashMap<Type,Type> INTERN = new HashMap<>(); + + // ---------------------------------------------------------- + // Simple types are implemented fully here. "Simple" means: the code and + // type hierarchy are simple, not that the Type is conceptually simple. + static final byte TBOT = 0; // Bottom (ALL) + static final byte TTOP = 1; // Top (ANY) + static final byte TCTRL = 2; // Ctrl flow bottom + static final byte TXCTRL = 3; // Ctrl flow top (mini-lattice: any-xctrl-ctrl-all) + static final byte TNIL = 4; // low null of all flavors + static final byte TXNIL = 5; // high or choice null + static final byte TSIMPLE = 6; // End of the Simple Types + + static final byte TPTR = 7; // All nil-able scalar values + static final byte TINT = 8; // All Integers; see TypeInteger + static final byte TFLT = 9; // All Floats ; see TypeFloat + static final byte TMEMPTR =10; // Memory pointer to a struct type + static final byte TFUNPTR =11; // Function pointer; unique signature and code address (just a bit) + static final byte TTUPLE =12; // Tuples; finite collections of unrelated Types, kept in parallel + static final byte TMEM =13; // All memory (alias 0) or A slice of memory - with specific alias + static final byte TSTRUCT =14; // Structs; tuples with named fields + static final byte TFLD =15; // Named fields in structs + static final byte TRPC =16; // Return Program Control (Return PC or RPC) + + public final byte _type; + + public boolean is_simple() { return _type < TSIMPLE; } + private static final String[] STRS = new String[]{"Bot","Top","Ctrl","~Ctrl","null","~nil"}; + protected Type(byte type) { _type = type; } + + public static final Type BOTTOM = new Type( TBOT ).intern(); // ALL + public static final Type TOP = new Type( TTOP ).intern(); // ANY + public static final Type CONTROL = new Type( TCTRL ).intern(); // Ctrl + public static final Type XCONTROL = new Type( TXCTRL ).intern(); // ~Ctrl + public static final Type NIL = new Type( TNIL ).intern(); // low null of all flavors + public static final Type XNIL = new Type( TXNIL ).intern(); // high or choice null + public static Type[] gather() { + ArrayList<Type> ts = new ArrayList<>(); + ts.add(BOTTOM); + ts.add(CONTROL); + ts.add(NIL); + ts.add(XNIL); + TypeNil.gather(ts); + TypePtr.gather(ts); + TypeInteger.gather(ts); + TypeFloat.gather(ts); + TypeMemPtr.gather(ts); + TypeFunPtr.gather(ts); + TypeMem.gather(ts); + Field.gather(ts); + TypeStruct.gather(ts); + TypeTuple.gather(ts); + TypeRPC.gather(ts); + int sz = ts.size(); + for( int i = 0; i < sz; i++ ) + ts.add(ts.get(i).dual()); + return ts.toArray(new Type[ts.size()]); + } + + // Is high or on the lattice centerline. + public boolean isHigh () { return _type==TTOP || _type==TXCTRL || _type==TXNIL; } + public boolean isHighOrConst() { return isHigh() || isConstant(); } + + // Strict constant values, things on the lattice centerline. + // Excludes both high and low values + public boolean isConstant() { return _type==TNIL; } + + // ---------------------------------------------------------- + + // Notes on Type interning: At the moment it is not easy to reset the + // interned types because we hold static references to several types and + // these are scattered around. This means the INTERN cache will retain all + // types from every run of the Parser. For this to work correctly types + // must be rigorous about defining when they are the same. Also types need + // to be immutable once defined. The rationale for interning is + // *correctness* with cyclic type definitions. Simple structural recursive + // checks go exponential with merely sharing, but with cycles they will + // stack overflow and crash. Interning means we do not need to have sharing + // checks with every type compare, only during interning. + + // Factory method which interns "this" + public <T extends Type> T intern() { + T nnn = (T)INTERN.get(this); + if( nnn==null ) + INTERN.put(nnn=(T)this,this); + return nnn; + } + + private int _hash; // Hash cache; not-zero when set. + @Override + public final int hashCode() { + if( _hash!=0 ) return _hash; + _hash = hash(); + if( _hash==0 ) _hash = 0xDEADBEEF; // Bad hash from subclass; use some junk thing + return _hash; + } + // Override in subclasses + int hash() { return _type; } + + @Override + public final boolean equals( Object o ) { + if( o==this ) return true; + if( !(o instanceof Type t)) return false; + if( _type != t._type ) return false; + return eq(t); + } + // Overridden in subclasses; subclass can assume "this!=t" and java classes are same + boolean eq(Type t) { return true; } + + + // ---------------------------------------------------------- + public final Type meet(Type t) { + // Shortcut for the self case + if( t == this ) return this; + // Same-type is always safe in the subclasses + if( _type==t._type ) return xmeet(t); + // TypeNil vs TypeNil meet + if( this instanceof TypeNil ptr0 && t instanceof TypeNil ptr1 ) + return ptr0.nmeet(ptr1); + // Reverse; xmeet 2nd arg is never "is_simple" and never equal to "this". + if( is_simple() ) return this.xmeet(t ); + if( t.is_simple() ) return t .xmeet(this); + return Type.BOTTOM; // Mixing 2 unrelated types + } + + // Compute meet right now. Overridden in subclasses. + // Handle cases where 'this.is_simple()' and unequal to 't'. + // Subclassed xmeet calls can assert that '!t.is_simple()'. + Type xmeet(Type t) { + assert is_simple(); // Should be overridden in subclass + // ANY meet anything is thing; thing meet ALL is ALL + if( _type==TBOT || t._type==TTOP ) return this; + if( _type==TTOP || t._type==TBOT ) return t; + + // RHS TypeNil vs NIL/XNIL + if( _type== TNIL ) return t instanceof TypeNil ptr ? ptr.meet0() : (t._type==TXNIL ? TypePtr.PTR : BOTTOM); + if( _type== TXNIL ) return t instanceof TypeNil ptr ? ptr.meetX() : (t._type== TNIL ? TypePtr.PTR : BOTTOM); + // 'this' is only {TCTRL,TXCTRL} + // Other non-simple RHS things bottom out + if( !t.is_simple() ) return BOTTOM; + // If RHS is NIL/XNIL + if( t._type==TNIL || t._type==TXNIL ) return BOTTOM; + // Both are {TCTRL,TXCTRL} and unequal to each other + return _type==TCTRL || t._type==TCTRL ? CONTROL : XCONTROL; + } + + public Type dual() { + return switch( _type ) { + case TBOT -> TOP; + case TTOP -> BOTTOM; + case TCTRL ->XCONTROL; + case TXCTRL-> CONTROL; + case TNIL ->XNIL; + case TXNIL -> NIL; + default -> throw Utils.TODO(); // Should not reach here + }; + } + + // ---------------------------------------------------------- + // Our lattice is defined with a MEET and a DUAL. + // JOIN is dual of meet of both duals. + public final Type join(Type t) { + if( this==t ) return this; + return dual().meet(t.dual()).dual(); + } + + // True if this "isa" t; e.g. 17 isa TypeInteger.BOT + public boolean isa( Type t ) { return meet(t)==t; } + + // True if this "isa" t up to named structures + public boolean shallowISA( Type t ) { return isa(t); } + + public Type nonZero() { return TypePtr.NPTR; } + + // Make a zero version of this type, 0 for integers and null for pointers. + public Type makeZero() { return Type.NIL; } + + /** Compute greatest lower bound in the lattice */ + public Type glb() { return Type.BOTTOM; } + + // Is forward-reference + public boolean isFRef() { return false; } + + // All reachable struct Fields are final + public boolean isFinal() { return true; } + + // Make all reachable struct Fields final + public Type makeRO() { return this; } + + // ---------------------------------------------------------- + + // Size in bits to hold an instance of this type. + // Sizes are expected to be between 1 and 64 bits. + // Size 0 means this either takes no space (such as a known-zero field) + // or isn't a scalar to be stored in memory. + public int log_size() { throw Utils.TODO(); } + + // ---------------------------------------------------------- + // Useful in the debugger, which calls toString everywhere. + // This is a more verbose dev-friendly print. + @Override + public final String toString() { + return print(new SB()).toString(); + } + + public SB print(SB sb) { return sb.p(str()); } + public SB gprint(SB sb) { return print(sb); } + + // This is used by error messages, and is a shorted print. + public String str() { return STRS[_type]; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/TypeFloat.java b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeFloat.java new file mode 100644 index 00000000..aa205149 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeFloat.java @@ -0,0 +1,95 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Float Type + */ +public class TypeFloat extends Type { + + public final static TypeFloat ONE = constant(1.0); + public final static TypeFloat FZERO = constant(0.0); + public final static TypeFloat F32 = make(32, 0); + public final static TypeFloat F64 = make(64, 0); + + // - high -64, high -32, con 0, low +32, low +64 + public final byte _sz; + + /** + * The constant value or 0 + */ + public final double _con; + + private TypeFloat(byte sz, double con) { + super(TFLT); + _sz = sz; + _con = con; + } + private static TypeFloat make(int sz, double con) { + return new TypeFloat((byte)sz,con).intern(); + } + + public static TypeFloat constant(double con) { return make(0, con); } + + public static void gather(ArrayList<Type> ts) { ts.add(F64); ts.add(F32); ts.add(constant(3.141592653589793)); } + + @Override public String str() { + return switch( _sz ) { + case -64 -> "~flt"; + case -32 -> "~f32"; + case 0 -> ""+_con+((float)_con==_con ? "f" : ""); + case 32 -> "f32"; + case 64 -> "flt"; + default -> throw Utils.TODO(); + }; + } + private boolean isF32() { return ((float)_con)==_con; } + + @Override public boolean isHigh () { return _sz< 0; } + @Override public boolean isConstant() { return _sz==0; } + @Override public int log_size() { return _sz==32 || _sz==-32 ? 2 : 3; } + + public double value() { return _con; } + + @Override + public TypeFloat xmeet(Type other) { + TypeFloat f = (TypeFloat)other; + // Invariant from caller: 'this' != 'other' and same class (TypeFloat). + TypeFloat i = (TypeFloat)other; // Contract + // Larger size in i1, smaller in i0 + TypeFloat i0 = _sz < i._sz ? this : i; + TypeFloat i1 = _sz < i._sz ? i : this; + + if( i1._sz== 64 ) return F64; + if( i0._sz==-64 ) return i1; + if( i1._sz== 32 ) + return i0._sz==0 && !i0.isF32() ? F64 : F32; + if( i1._sz!= 0 ) return i1; + // i1 is a constant + if( i0._sz==-32 ) + return i1.isF32() ? i1 : F64; + // Since both are constants, and are never equals (contract) unequals + // constants fall to bottom + return i0.isF32() && i1.isF32() ? F32 : F64; + } + + @Override + public Type dual() { + return isConstant() ? this : make(-_sz,0); // Constants are a self-dual + } + + @Override public Type makeZero() { return FZERO; } + @Override public Type glb() { return F64; } + + @Override + int hash() { return (int)(Double.hashCode(_con) ^ _sz ^ (1<<17)); } + @Override + public boolean eq( Type t ) { + TypeFloat i = (TypeFloat)t; // Contract + // Allow NaN to check for equality + return _sz==i._sz && Double.doubleToLongBits(_con)==Double.doubleToLongBits(i._con); + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java new file mode 100644 index 00000000..be801c85 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java @@ -0,0 +1,104 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Represents a Pointer to a function. + * <p></p> + * Functions have argument types and a return type, making a function + * signature. Within a signature there can be many instances of functions, and + * each function is labeled with a small integer constant. A TFP can represent + * a single signature and a set of functions; the set might contain only 1 + * function or the zeroth function (the null ptr). + * <p></p> + * Functions with different signatures cannot be mixed, and will result in a + * bottom function type, which can only be null-checked. + */ +public class TypeFunPtr extends TypeNil { + // A TypeFunPtr is Signature and a set of functions. + public final TypeTuple _sig; + public final Type _ret; + // Cheesy easy implementation for a small set; 1 bit per unique function + // within the TypeTuple. Can be upgraded to a BitSet for larger classes of + // functions. Negative means "these 63 concrete bits plus infinite unknown more" + public final long _fidxs; // 63 unique functions per signature + + private TypeFunPtr(byte nil, TypeTuple sig, Type ret, long fidxs) { + super(TFUNPTR,nil); + assert sig != null; + _sig = sig; + _ret = ret; + _fidxs = fidxs; + } + + public static TypeFunPtr make( byte nil, TypeTuple sig, Type ret, long fidxs ) { return new TypeFunPtr(nil,sig,ret,fidxs).intern(); } + public static TypeFunPtr make( boolean nil, TypeTuple sig, Type ret ) { return make((byte)(nil ? 3 : 2),sig,ret,-1); } + @Override TypeFunPtr makeFrom( byte nil ) { return nil ==_nil ? this : make( nil,_sig,_ret, _fidxs); } + public TypeFunPtr makeFrom( Type ret ) { return ret ==_ret ? this : make( _nil,_sig, ret, _fidxs); } + public TypeFunPtr makeFrom( int fidx ) { return make((byte)2, _sig,_ret,1L<<fidx ); } + + public static TypeFunPtr BOT = make((byte)3,TypeTuple.BOT,Type.BOTTOM,-1); + public static TypeFunPtr TEST = make((byte)2,TypeTuple.TEST,TypeInteger.BOT,1); + public static TypeFunPtr TEST0 = make((byte)3,TypeTuple.TEST,TypeInteger.BOT,3); + public static void gather(ArrayList<Type> ts) { ts.add(TEST); ts.add(TEST0); ts.add(BOT); } + + @Override + Type xmeet(Type t) { + TypeFunPtr that = (TypeFunPtr) t; + return TypeFunPtr.make(xmeet0(that),(TypeTuple)_sig.meet(that._sig), _ret.meet(that._ret), _fidxs | that._fidxs); + } + + @Override + public TypeFunPtr dual() { return TypeFunPtr.make(dual0(), _sig.dual(), _ret.dual(), ~_fidxs); } + + // RHS is NIL; do not deep-dual when crossing the centerline + @Override public Type meet0() { return _nil==3 ? this : make((byte)3,_sig,_ret,_fidxs); } + + @Override public TypeFunPtr glb() { return make((byte)3,_sig,_ret,-1L); } + + @Override public boolean isHigh () { return _nil <= 1 || (_nil==2 && _fidxs==0); } + @Override public boolean isConstant() { return (_nil==2 && Long.bitCount(_fidxs)==1) || (_nil==3 && _fidxs==0); } + + @Override public int log_size() { return 2; } // (1<<2)==4-byte pointers + + public Type arg(int i) { return _sig._types[i]; } + public long fidxs() { return _fidxs; } + public Type ret() { return _ret; } + public int nargs() { return _sig._types.length; } + + @Override + int hash() { return Utils.fold(_sig.hashCode() ^ _ret.hashCode() ^ _fidxs ^ super.hash()); } + + @Override + boolean eq(Type t) { + TypeFunPtr ptr = (TypeFunPtr)t; // Invariant + return _sig == ptr._sig && _ret == ptr._ret && _fidxs == ptr._fidxs && super.eq(ptr); + } + + @Override public String str() { return print(new SB()).toString(); } + @Override public SB print(SB sb) { return _print(sb,false,true); } + public SB print(SB sb, boolean n) { return _print(sb,false,n); } + @Override public SB gprint(SB sb) { return _print(sb,true ,true); } + private static SB _print(SB sb, boolean g, Type t) { return g ? t.gprint(sb) : t.print(sb); } + private SB _print(SB sb, boolean g, boolean n) { + sb.p(x()).p("{ "); + if( _sig._types!=null ) + for( Type t : _sig._types ) + _print(sb,g,t).p(" "); + _print(sb.p(g ? "→ " : "-> "),g,_ret).p(" #"); + if( isHigh() ) sb.p("~"); + long fidxs = isHigh() ? ~_fidxs : _fidxs; + String fidx = fidxs==0 ? "" + : Long.bitCount(fidxs) == 1 ? ""+Long.numberOfTrailingZeros(fidxs) + : fidxs == -1 ? "ALL" + : "b"+Long.toBinaryString(fidxs); // Just some function bits + return sb.p(fidx).p("}").p(q()); + } + + // Usage: for( long fidxs=fidxs(); fidxs!=0; fidxs=nextFIDX(fidxs) { int fidxs = Long.numberOfTrailingZeros(fidxs); ... } + public static long nextFIDX(long fidxs) { return fidxs & (fidxs-1); } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeInteger.java new file mode 100644 index 00000000..cccb85a5 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeInteger.java @@ -0,0 +1,128 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Integer Type + */ +public class TypeInteger extends Type { + + public final static TypeInteger ZERO= make(0,0); + public final static TypeInteger FALSE=ZERO; + public final static TypeInteger TRUE= make(1,1); + + public final static TypeInteger I1 = make(-1,0); + public final static TypeInteger I8 = make(-128,127); + public final static TypeInteger I16 = make(-32768,32767); + public final static TypeInteger I32 = make(-1L<<31,(1L<<31)-1); + public final static TypeInteger BOT = make(Long.MIN_VALUE,Long.MAX_VALUE); + public final static TypeInteger TOP = BOT.dual(); + + public final static TypeInteger U1 = make(0,1); + public final static TypeInteger BOOL= U1; + public final static TypeInteger U8 = make(0,255); + public final static TypeInteger U16 = make(0,65535); + public final static TypeInteger U32 = make(0,(1L<<32)-1); + + /** + * Describes an integer *range* - everything from min to max; both min and + * max are inclusive. If min==max, this is a constant. + * <br> + * If min <= max, this is a below center (towards bottom). + * If min > max, this is an above center (towards top). + */ + public final long _min, _max; + + private TypeInteger(long min, long max) { + super(TINT); + _min = min; + _max = max; + } + + // Strict non-zero contract + public static TypeInteger make(long lo, long hi) { return new TypeInteger(lo,hi).intern(); } + + public static TypeInteger constant(long con) { return make(con, con); } + + public static void gather(ArrayList<Type> ts) { ts.add(I32); ts.add(BOT); ts.add(U1); ts.add(I1); ts.add(U8); } + + @Override public String str() { + if( isConstant() ) return ""+_min; + long lo = _min, hi = _max; + String x = ""; + if( hi < lo ) { + lo = _max; hi = _min; + x = "~"; + } + return x+_str(lo,hi); + } + private static String _str(long lo, long hi) { + if( lo==Long.MIN_VALUE && hi==Long.MAX_VALUE ) return "int"; + if( lo== 0 && hi== 1 ) return "bool"; + if( lo== -1 && hi== 0 ) return "i1"; + if( lo== -128 && hi== 127 ) return "i8"; + if( lo== -32768 && hi== 32767 ) return "i16"; + if( lo== -1L<<31 && hi==(1L<<31)-1 ) return "i32"; + if( lo== 0 && hi== 255 ) return "u8"; + if( lo== 0 && hi== 65535 ) return "u16"; + if( lo== 0 && hi==(1L<<32)-1 ) return "u32"; + return "["+lo+"-"+hi+"]"; + } + + @Override public boolean isHigh () { return _min > _max; } + @Override public boolean isConstant() { return _min == _max; } + + @Override public int log_size() { + if( this==I8 || this==U8 || this==BOOL ) return 0; // 1<<0 == 1 bytes + if( this==I16 || this==U16 ) return 1; // 1<<1 == 2 bytes + if( this==I32 || this==U32 ) return 2; // 1<<2 == 4 bytes + if( this==BOT ) return 3; // 1<<3 == 8 bytes + if( isHighOrConst() ) return 0; + throw Utils.TODO(); + } + + public long value() { assert isConstant(); return _min; } + + // AND-mask of forced zeros. e.g. unsigned types will return their mask; + // u8 will return 0xFF. But also a range of 16-18 (0x10-0x12) will return + // 0x13 - no value in the range {16,17,18} will allow bit 0x04 to be set. + public long mask() { + if( isHigh() ) return 0; + if( isConstant() ) return _min; + // Those bit positions which differ min to max + long x = _min ^ _max; + // Highest '1' bit in the differ set. Since the range is from min to + // max, all values below here are possible. + long ff1 = Long.highestOneBit(x); + // Make a all-1's mask from ff1, and set over the same bits (either min + // or max is ok). + long mask = _min | (ff1-1) | ff1; + return mask; + } + + @Override + public Type xmeet(Type other) { + // Invariant from caller: 'this' != 'other' and same class (TypeInteger) + TypeInteger i = (TypeInteger)other; // Contract + return make(Math.min(_min,i._min), Math.max(_max,i._max)); + } + + @Override public TypeInteger dual() { return make(_max,_min); } + + @Override public TypeInteger nonZero() { + if( isHigh() ) return this; + if( this==ZERO ) return null; // No sane answer + if( _min==0 ) return make(1,Math.max(_max,1)); // specifically good on BOOL + if( _max==0 ) return make(_min,-1); + return this; + } + @Override public Type makeZero() { return ZERO; } + @Override public TypeInteger glb() { return BOT; } + @Override int hash() { return Utils.fold(_min) * Utils.fold(_max); } + @Override public boolean eq( Type t ) { + TypeInteger i = (TypeInteger)t; // Contract + return _min==i._min && _max==i._max; + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeMem.java new file mode 100644 index 00000000..9ed4c7a8 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeMem.java @@ -0,0 +1,74 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import java.lang.Long; +import java.util.BitSet; +import java.util.ArrayList; + + +/** + * Represents a slice of memory corresponding to a set of aliases + */ +public class TypeMem extends Type { + + // Which slice of memory? + // 0 means TOP, no slice. + // 0 means BOT, all memory. + // N means slice#N. + public final int _alias; + public final Type _t; // Memory contents, some scalar type + + private TypeMem(int alias, Type t) { + super(TMEM); + assert alias!=0 || (t==Type.TOP || t==Type.BOTTOM); + _alias = alias; + _t = t; + } + + public static TypeMem make(int alias, Type t) { return new TypeMem(alias,t).intern(); } + public static final TypeMem TOP = make(0, Type.TOP ); + public static final TypeMem BOT = make(0, Type.BOTTOM); + + public static void gather(ArrayList<Type> ts) { ts.add(make(1,Type.NIL)); ts.add(make(1,TypeInteger.ZERO)); ts.add(BOT); } + + @Override + TypeMem xmeet(Type t) { + TypeMem that = (TypeMem) t; // Invariant: TypeMem and unequal + if( this==TOP ) return that; + if( that==TOP ) return this; + if( this==BOT ) return BOT; + if( that==BOT ) return BOT; + int alias = _alias==that._alias ? _alias : 0; + Type mt = _t.meet(that._t); + return make(alias,mt); + } + + @Override + public Type dual() { + return make(_alias,_t.dual()); + } + + @Override public boolean isHigh() { return _t.isHigh(); } + @Override public Type glb() { return make(_alias,_t.glb()); } + + @Override int hash() { return 9876543 + _alias + _t.hashCode(); } + + @Override boolean eq(Type t) { + TypeMem that = (TypeMem) t; // Invariant + return _alias == that._alias && _t == that._t; + } + + @Override public SB print(SB sb) { + sb.p("#"); + if( _alias==0 ) return sb.p(_t._type==TTOP ? "TOP" : "BOT"); + return _t.print(sb.p(_alias).p(":")); + } + @Override public SB gprint(SB sb) { + sb.p("#"); + if( _alias==0 ) return sb.p(_t._type==TTOP ? "TOP" : "BOT"); + return _t.gprint(sb.p(_alias).p(":")); + } + + @Override public String str() { return toString(); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java new file mode 100644 index 00000000..d7a8b17d --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java @@ -0,0 +1,105 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Represents a Pointer to memory. + * + * Null is generic pointer to non-existent memory. + * *void is a non-Null pointer to all possible refs, both structs and arrays. + * Pointers can be to specific struct and array types, or a union with Null. + * The distinguished *$BOT ptr represents union of *void and Null. + * The distinguished *$TOP ptr represents the dual of *$BOT. + */ +public class TypeMemPtr extends TypeNil { + // A TypeMemPtr is pair (obj,nil) + // where obj is a TypeStruct, possibly TypeStruct.BOT/TOP + // where nil is an explicit null is allowed or not + + // Examples: + // (Person,false) - a not-nil Person + // (Person,true ) - a Person or a nil + // (BOT ,false) - a not-nil void* (unspecified struct) + // (TOP ,true ) - a nil + + public final TypeStruct _obj; + + private TypeMemPtr(byte nil, TypeStruct obj) { + super(TMEMPTR,nil); + assert obj!=null; + _obj = obj; + } + static TypeMemPtr make(byte nil, TypeStruct obj) { return new TypeMemPtr(nil, obj).intern(); } + public static TypeMemPtr makeNullable(TypeStruct obj) { return make((byte)3, obj); } + public static TypeMemPtr make(TypeStruct obj) { return new TypeMemPtr((byte)2, obj).intern(); } + public TypeMemPtr makeFrom(TypeStruct obj) { return obj==_obj ? this : make(_nil, obj); } + public TypeMemPtr makeNullable() { return makeFrom((byte)3); } + @Override TypeMemPtr makeFrom(byte nil) { return nil==_nil ? this : make(nil,_obj); } + @Override public TypeMemPtr makeRO() { return makeFrom(_obj.makeRO()); } + @Override public boolean isFinal() { return _obj.isFinal(); } + + // An abstract pointer, pointing to either a Struct or an Array. + // Can also be null or not, so 4 choices {TOP,BOT} x {nil,not} + public static TypeMemPtr BOT = make((byte)3, TypeStruct.BOT); + public static TypeMemPtr TOP = BOT.dual(); + public static TypeMemPtr NOTBOT = make((byte)2,TypeStruct.BOT); + + public static TypeMemPtr TEST= make((byte)2, TypeStruct.TEST); + public static void gather(ArrayList<Type> ts) { ts.add(NOTBOT); ts.add(BOT); ts.add(TEST); } + + @Override + public TypeNil xmeet(Type t) { + TypeMemPtr that = (TypeMemPtr) t; + return TypeMemPtr.make(xmeet0(that), (TypeStruct)_obj.meet(that._obj)); + } + + @Override + public TypeMemPtr dual() { return TypeMemPtr.make( dual0(), _obj.dual()); } + + // RHS is NIL; do not deep-dual when crossing the centerline + @Override Type meet0() { return _nil==3 ? this : make((byte)3,_obj); } + + + // True if this "isa" t up to named structures + @Override public boolean shallowISA( Type t ) { + if( !(t instanceof TypeMemPtr that) ) return false; + if( this==that ) return true; + if( xmeet0(that)!=that._nil ) return false; + if( _obj==that._obj ) return true; + if( _obj._name.equals(that._obj._name) ) + return true; // Shallow, do not follow matching names, just assume ok + throw Utils.TODO(); // return _obj.shallowISA(that._obj); + } + + @Override public TypeMemPtr glb() { return make((byte)3,_obj.glb()); } + // Is forward-reference + @Override public boolean isFRef() { return _obj.isFRef(); } + + @Override public int log_size() { return 2; } // (1<<2)==4-byte pointers + + @Override int hash() { return _obj.hashCode() ^ super.hash(); } + + @Override boolean eq(Type t) { + TypeMemPtr ptr = (TypeMemPtr)t; // Invariant + return _obj == ptr._obj && super.eq(ptr); + } + + @Override public String str() { + if( this== NOTBOT) return "*void"; + if( this== BOT) return "*void?"; + return x()+"*"+_obj.str()+q(); + } + + @Override public SB print(SB sb) { + if( this== NOTBOT) return sb.p("*void"); + if( this== BOT) return sb.p("*void?"); + return _obj.print(sb.p(x()).p("*")).p(q()); + } + @Override public SB gprint(SB sb) { + if( this== NOTBOT) return sb.p("*void"); + if( this== BOT) return sb.p("*void?"); + return _obj.gprint(sb.p(x()).p("*")).p(q()); + } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/TypeNil.java b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeNil.java new file mode 100644 index 00000000..aee623ba --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeNil.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Nil-able Scalar types + */ +public abstract class TypeNil extends Type { + // 0 = high-subclass choice nil + // 1 = high-subclass no nil + // 2 = low -subclass no nil + // 3 = low -subclass also nil + final byte _nil; + + TypeNil(byte t, byte nil ) { super(t); _nil = nil; } + + public static void gather(ArrayList<Type> ts) { } + + abstract TypeNil makeFrom(byte nil); + + byte xmeet0(TypeNil that) { return (byte)Math.max(_nil,that._nil); } + + byte dual0() { return (byte)(3-_nil); } + + // RHS is NIL + abstract Type meet0(); + + // RHS is XNIL + Type meetX() { + return _nil==0 ? XNIL : (_nil<=2 ? TypePtr.NPTR : TypePtr.PTR); + } + + Type nmeet(TypeNil tn) { + // Invariants: both are TypeNil subclasses and unequal classes. + // If this is TypePtr, we went to TypePtr.nmeet and not here. + // If that is TypePtr, this is not (invariant); reverse and go again. + if( tn instanceof TypePtr ts ) return ts.nmeet(this); + + // Two mismatched TypeNil, no Scalar. + if( _nil==0 && tn._nil==0 ) return XNIL; + if( _nil<=2 && tn._nil<=2 ) return TypePtr.NPTR; + return TypePtr.PTR; + } + + @Override public boolean isHigh () { return _nil <= 1; } + @Override public boolean isConstant () { return false; } + @Override public boolean isHighOrConst() { return isHigh() || isConstant(); } + + @Override public Type glb() { return Type.NIL; } + + public boolean notNull() { return _nil==1 || _nil==2; } + public boolean nullable() { return _nil==3; } + + final String q() { return _nil == 1 || _nil == 2 ? "" : "?"; } + final String x() { return isHigh() ? "~" : ""; } + + int hash() { return _nil<<17; } + + boolean eq(TypeNil ptr) { return _nil == ptr._nil; } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/TypePtr.java b/chapter20/src/main/java/com/seaofnodes/simple/type/TypePtr.java new file mode 100644 index 00000000..e9091c25 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/TypePtr.java @@ -0,0 +1,53 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Represents a Scalar; a single register-sized value. + */ +public class TypePtr extends TypeNil { + + private TypePtr(byte nil) { super(TPTR,nil); } + + // An abstract pointer, pointing to either a Struct or an Array. + // Can also be null or not, so 4 choices {TOP,BOT} x {nil,not} + public static TypePtr XPTR = new TypePtr((byte)0).intern(); + public static TypePtr XNPTR= new TypePtr((byte)1).intern(); + public static TypePtr NPTR = new TypePtr((byte)2).intern(); + public static TypePtr PTR = new TypePtr((byte)3).intern(); + private static final TypePtr[] PTRS = new TypePtr[]{XPTR,XNPTR,NPTR,PTR}; + + public static void gather(ArrayList<Type> ts) { ts.add(PTR); ts.add(NPTR); } + + TypeNil makeFrom(byte nil) { throw Utils.TODO(); } + + @Override public TypeNil xmeet(Type t) { + TypePtr that = (TypePtr) t; + return PTRS[xmeet0(that)]; + } + + @Override public TypePtr dual() { return PTRS[dual0()]; } + + // High scalar loses, low scalar wins + @Override TypeNil nmeet(TypeNil tn) { + if( _nil==0 ) return tn; // High scalar loses + if( _nil==1 ) return tn.makeFrom(xmeet0(tn)); // High scalar loses + if( _nil==2 ) return tn._nil==3 ? PTR : NPTR; // Low scalar wins + return PTR; // this + } + + + // RHS is NIL + @Override Type meet0() { return isHigh() ? NIL : PTR; } + // RHS is XNIL + // 0->xscalar, 1->nscalar, 2->nscalar, 3->scalar + @Override Type meetX() { return _nil==0 ? XNIL : (_nil==3 ? PTR : NPTR); } + + @Override public TypePtr glb() { return PTR; } + + private static final String[] STRS = new String[]{"~ptr","~nptr","nptr","ptr"}; + @Override public String str() { return STRS[_nil]; } + @Override public SB print(SB sb) { return sb.p(str()); } +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/TypeRPC.java b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeRPC.java new file mode 100644 index 00000000..ec2f5f0a --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeRPC.java @@ -0,0 +1,100 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashSet; + +/** + * Return Program Control or Return PC or RPC + */ +public class TypeRPC extends Type { + + // A set of CallEndNode IDs (or StopNode); commonly just one. + // Basically a sparse bit set + final HashSet<Integer> _rpcs; + + // If true, invert the meaning of the bits + final boolean _any; + + private TypeRPC(boolean any, HashSet<Integer> rpcs) { + super(TRPC); + _any = any; + _rpcs = rpcs; + } + private static TypeRPC make(boolean any, HashSet<Integer> rpcs) { + return new TypeRPC(any,rpcs).intern(); + } + + public static TypeRPC constant(int cend) { + HashSet<Integer> rpcs = new HashSet<>(); + rpcs.add(cend); + return make(false,rpcs); + } + + public static final TypeRPC BOT = make(true,new HashSet<>()); + private static final TypeRPC TEST2 = constant(2); + private static final TypeRPC TEST3 = constant(2); + + public static void gather(ArrayList<Type> ts) { ts.add(BOT); ts.add(TEST2); ts.add(TEST3); } + + @Override public String str() { + if( _rpcs.isEmpty() ) + return _any ? "$[ALL]" : "$[]"; + if( _rpcs.size()==1 ) + for( Integer rpc : _rpcs ) + return _any ? "$[-"+rpc+"]" : "$["+rpc+"]"; + return "$["+(_any ? "-" : "")+_rpcs+"]"; + } + + @Override public boolean isConstant() { + return !_any && _rpcs.size()==1; + } + + @Override + public TypeRPC xmeet(Type other) { + TypeRPC rpc = (TypeRPC)other; + // If the two sets are equal, the _any must be unequal (invariant), + // so they cancel and all bits are set. + if( _rpcs.equals(rpc._rpcs) ) + return BOT; + // Classic union of bit sets (which might be infinite). + HashSet<Integer> lhs = _rpcs, rhs = rpc._rpcs; + // Smaller on left + if( lhs.size() > rhs.size() ) { lhs = rpc._rpcs; rhs = _rpcs; } + + HashSet<Integer> rpcs = new HashSet<>(); + boolean any = true; + // If both sets are infinite, intersect. + if( _any && rpc._any ) { + for( Integer i : lhs ) if( rhs.contains(i) ) rpcs.add(i); + + } else if( !_any && !rpc._any ) { + // if neither set is infinite, union. + rpcs.addAll( lhs ); + rpcs.addAll( rhs ); + any = false; + } else { + // If one is infinite, subtract the other from it. + HashSet<Integer> sub = _any ? rpc._rpcs : _rpcs; + HashSet<Integer> inf = _any ? _rpcs : rpc._rpcs; + for( Integer i : inf ) + if( inf.contains(i) && !sub.contains(i) ) + rpcs.add(i); + } + return make(any,rpcs); + } + + @Override + public Type dual() { return make(!_any,_rpcs); } + + @Override + int hash() { return _rpcs.hashCode() ^ (_any ? -1 : 0) ; } + @Override + public boolean eq( Type t ) { + TypeRPC rpc = (TypeRPC)t; // Contract + return _any==rpc._any && _rpcs.equals(rpc._rpcs); + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/TypeStruct.java b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeStruct.java new file mode 100644 index 00000000..6c5cb8d4 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeStruct.java @@ -0,0 +1,248 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import com.seaofnodes.simple.Utils; +import java.util.ArrayList; + +/** + * Represents a struct type. + */ +public class TypeStruct extends Type { + + // A Struct has a name and a set of fields; the fields themselves have + // names, types and aliases. The name has no semantic meaning, but is + // useful for debugging. Briefly during parsing a Struct can be a + // forward-ref; in this case the _fields array is null. + + // Its illegal to attempt to load a field from a forward-ref struct. + + // Example: "int rez = new S.x; struct S { int x; } return rez;" // Error, S not defined + // Rewrite: "struct S { int x; } int rez = new S.x; return rez;" // Ok, no forward ref + // + // During the normal optimization run, struct types "bottom out" at further + // struct references, so we don't have to handle e.g. cyclic types. The + // "bottom out" is again the forward-ref struct. + public final String _name; + public final Field[] _fields; + + TypeStruct(String name, Field[] fields) { + super(TSTRUCT); + _name = name; + _fields = fields; + } + + // All fields directly listed + public static TypeStruct make(String name, Field... fields) { return new TypeStruct(name, fields).intern(); } + public static final TypeStruct TOP = make("$TOP",new Field[0]); + public static final TypeStruct BOT = make("$BOT",new Field[0]); + public static final TypeStruct TEST = make("test",new Field[]{Field.TEST}); + // Forward-ref version + public static TypeStruct makeFRef(String name) { return make(name, (Field[])null); } + // Make a read-only version + @Override public TypeStruct makeRO() { + if( isFinal() ) return this; + Field[] flds = new Field[_fields.length]; + for( int i=0; i<flds.length; i++ ) + flds[i] = _fields[i].makeRO(); + return make(_name,flds); + } + + // Array + public static TypeStruct makeAry(TypeInteger len, int lenAlias, Type body, int bodyAlias) { + assert body instanceof TypeInteger || body instanceof TypeFloat || (body instanceof TypeNil tn && tn.nullable()); + assert len.isa(TypeInteger.U32); + return make("[" + body.str() + "]", + Field.make("#" ,len , lenAlias,true ), + Field.make("[]",body,bodyAlias,false)); + } + + // A pair of self-cyclic types + private static final TypeStruct S1F = makeFRef("S1"); + private static final TypeStruct S2F = makeFRef("S2"); + public static final TypeStruct S1 = make("S1", Field.make("a", TypeInteger.BOT, -1, false), Field.make("s2",TypeMemPtr.make((byte)2,S2F),-2, false) ); + private static final TypeStruct S2 = make("S2", Field.make("b", TypeFloat .F64, -3, false), Field.make("s1",TypeMemPtr.make((byte)2,S1F),-4, false) ); + + private static final TypeStruct ARY = makeAry(TypeInteger.U32,-1,TypeInteger.BOT,-2); + + public static void gather(ArrayList<Type> ts) { ts.add(TEST); ts.add(BOT); ts.add(S1); ts.add(S2); ts.add(ARY); } + + // Find field index by name + public int find(String fname) { + for( int i=0; i<_fields.length; i++ ) + if( _fields[i]._fname.equals(fname) ) + return i; + return -1; + } + public Field field(String fname) { + int idx = find(fname); + return idx== -1 ? null : _fields[idx]; + } + + // Find field index by alias + public int findAlias( int alias ) { + for( int i=0; i<_fields.length; i++ ) + if( _fields[i]._alias==alias ) + return i; + return -1; + } + + + @Override + Type xmeet(Type t) { + TypeStruct that = (TypeStruct) t; + if( this==TOP ) return that; + if( that==TOP ) return this; + if( this==BOT ) return BOT; + if( that==BOT ) return BOT; + + // Within the same compilation unit, struct names are unique. If the + // names differ, its different structs. Across many compilation units, + // structs with the same name but different field layouts can be + // interned... which begs the question: + // "What is the meet of structs from two different compilation units?" + // And the answer is: "don't ask". + if( !_name.equals(that._name) ) + return BOT; // It's a struct; that's about all we know + if( this._fields==null ) return that; + if( that._fields==null ) return this; + if( _fields.length != that._fields.length ) return BOT; + // Just do field meets + Field[] flds = new Field[_fields.length]; + for( int i=0; i<_fields.length; i++ ) { + Field f0 = _fields[i], f1 = that._fields[i]; + if( !f0._fname.equals(f1._fname) || f0._alias != f1._alias ) + return BOT; + flds[i] = (Field)f0.meet(f1); + } + return make(_name,flds); + } + + @Override + public TypeStruct dual() { + if( this==TOP ) return BOT; + if( this==BOT ) return TOP; + if( _fields == null ) return this; + Field[] flds = new Field[_fields.length]; + for( int i=0; i<_fields.length; i++ ) + flds[i] = _fields[i].dual(); + return make(_name,flds); + } + + // Keeps the same struct, but lower-bounds all fields. + @Override public TypeStruct glb() { + if( _glb() ) return this; + // Need to glb each field + Field[] flds = new Field[_fields.length]; + for( int i=0; i<_fields.length; i++ ) + flds[i] = _fields[i].glb(); + return make(_name,flds); + } + private boolean _glb() { + if( _fields!=null ) + for( Field f : _fields ) + if( f.glb() != f ) + return false; + return true; + } + + // Is forward-reference + @Override public boolean isFRef() { return _fields==null; } + // All fields are final + @Override public boolean isFinal() { + if( _fields==null ) return true; + for( Field fld : _fields ) + if( !fld.isFinal() ) + return false; + return true; + } + + @Override + boolean eq(Type t) { + TypeStruct ts = (TypeStruct)t; // Invariant + if( !_name.equals(ts._name) ) + return false; + if( _fields == ts._fields ) return true; + if( _fields==null || ts._fields==null ) return false; + if( _fields.length!=ts._fields.length ) + return false; + for( int i = 0; i < _fields.length; i++ ) + if( _fields[i] != ts._fields[i] ) + return false; + return true; + } + + @Override + int hash() { + long hash = _name.hashCode(); + if( _fields != null ) + for( Field f : _fields ) + hash = Utils.rot(hash,13) ^ f.hashCode(); + return Utils.fold(hash); + } + + @Override + public SB print(SB sb) { + sb.p(_name); + if( _fields == null ) return sb; // Forward reference struct, just print the name + sb.p(" {"); + for( Field f : _fields ) + f._type.print(sb).p(f._final ? " " : " !").p(f._fname).p("; "); + return sb.p("}"); + } + @Override public SB gprint( SB sb ) { return sb.p(_name); } + + @Override public String str() { return _name; } + + + public boolean isAry() { return _fields.length==2 && _fields[1]._fname.equals("[]"); } + + public int aryBase() { + assert isAry(); + if( _offs==null ) _offs = offsets(); + return _offs[1]; + } + public int aryScale() { + assert isAry(); + return _fields[1]._type.log_size(); + } + + + // Field offsets as packed byte offsets + private int[] _offs; // Lazily computed and not part of the type semantics + public int offset(int idx) { + if( _offs==null ) _offs = offsets(); + return _offs[idx]; + } + private int[] offsets() { // Field byte offsets + // Compute a layout for a collection of fields + assert _fields != null; // No forward refs + + // Array layout is different: len,[pad],body... + if( isAry() ) + return _offs = new int[]{ 0, _fields[1]._type.log_size() < 3 ? 4 : 8 }; + + // Compute a layout + int[] cnts = new int[4]; // Count of fields at log field size + for( Field f : _fields ) + cnts[f._type.log_size()]++; // Log size is 0(byte), 1(i16/u16), 2(i32/f32), 3(i64/dbl) + int off = 0, idx = 0; // Base common struct fields go here, e.g. Mark/Klass + // Compute offsets to the start of each power-of-2 aligned fields. + int[] offs = new int[4]; + for( int i=3; i>=0; i-- ) { + offs[i] = off; + off += cnts[i]<<i; + } + // Assign offsets to all fields. + // Really a hidden radix sort. + _offs = new int[_fields.length+1]; + for( Field f : _fields ) { + int log = f._type.log_size(); + _offs[idx++] = offs[log]; // Field offset + offs[log] += 1<<log; // Next field offset at same alignment + cnts[log]--; // Count down, should be all zero at end + } + _offs[_fields.length] = (off+7)& ~7; // Round out to max alignment + return _offs; + } + +} diff --git a/chapter20/src/main/java/com/seaofnodes/simple/type/TypeTuple.java b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeTuple.java new file mode 100644 index 00000000..6d3a2b69 --- /dev/null +++ b/chapter20/src/main/java/com/seaofnodes/simple/type/TypeTuple.java @@ -0,0 +1,97 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.SB; +import java.util.ArrayList; + +public class TypeTuple extends Type { + + public final Type[] _types; + + private TypeTuple(Type[] types) { super(TTUPLE); _types = types; } + public static TypeTuple make(Type... types) { return new TypeTuple(types).intern(); } + + public static final TypeTuple BOT = new TypeTuple(new Type[0]).intern(); + public static final TypeTuple TOP = new TypeTuple(null).intern(); + + public static final TypeTuple TEST = make(TypeInteger.BOT,TypeMemPtr.TEST); + public static final TypeTuple START= make(Type.CONTROL,TypeMem.TOP,TypeInteger.BOT); + public static final TypeTuple MAIN = make(TypeInteger.BOT); + public static final TypeTuple RET = make(Type.CONTROL,TypeMem.BOT,Type.BOTTOM); + + public static final TypeTuple IF_BOTH = make(new Type[]{Type. CONTROL,Type. CONTROL}); + public static final TypeTuple IF_NEITHER = make(new Type[]{Type.XCONTROL,Type.XCONTROL}); + public static final TypeTuple IF_TRUE = make(new Type[]{Type. CONTROL,Type.XCONTROL}); + public static final TypeTuple IF_FALSE = make(new Type[]{Type.XCONTROL,Type. CONTROL}); + + public static void gather(ArrayList<Type> ts) { ts.add(BOT); ts.add(TEST); ts.add(START); ts.add(MAIN); ts.add(IF_TRUE); } + + @Override + Type xmeet(Type other) { + TypeTuple tt = (TypeTuple)other; // contract from xmeet + if( this==TOP ) return other; + if( tt ==TOP ) return this ; + if( _types.length != tt._types.length ) + return BOT; + Type[] ts = new Type[_types.length]; + for( int i=0; i<_types.length; i++ ) + ts[i] = _types[i].meet(tt._types[i]); + return make(ts); + } + + @Override public TypeTuple dual() { + if( this==TOP ) return BOT; + if( this==BOT ) return TOP; + Type[] ts = new Type[_types.length]; + for( int i=0; i<_types.length; i++ ) + ts[i] = _types[i].dual(); + return make(ts); + } + + @Override + public Type glb() { + Type[] ts = new Type[_types.length]; + for( int i=0; i<_types.length; i++ ) + ts[i] = _types[i].glb(); + return make(ts); + } + + @Override public String str() { return print(new SB()).toString(); } + + @Override public SB print(SB sb) { + if( this==TOP ) return sb.p("[TOP]"); + if( this==BOT ) return sb.p("[BOT]"); + sb.p("[ "); + for( Type t : _types ) + t.print(sb).p(", "); + return sb.unchar(2).p("]"); + } + @Override public SB gprint(SB sb) { + if( this==TOP ) return sb.p("[TOP]"); + if( this==BOT ) return sb.p("[BOT]"); + sb.p("[ "); + for( Type t : _types ) + t.gprint(sb).p(", "); + return sb.unchar(2).p("]"); + } + + @Override + int hash() { + int sum = 0; + if( _types!=null ) for( Type type : _types ) sum ^= type.hashCode(); + return sum; + } + + @Override + boolean eq( Type t ) { + TypeTuple tt = (TypeTuple)t; // Contract + if( _types==null && tt._types==null ) return true; + if( _types==null || tt._types==null ) return false; + if( _types.length != tt._types.length ) return false; + for( int i=0; i<_types.length; i++ ) + if( _types[i]!=tt._types[i] ) + return false; + return true; + } + + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/BrainFuckTest.java b/chapter20/src/test/java/com/seaofnodes/simple/BrainFuckTest.java new file mode 100644 index 00000000..8f09cb3d --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/BrainFuckTest.java @@ -0,0 +1,69 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.evaluator.Evaluator; +import java.nio.charset.StandardCharsets; +import org.junit.Test; +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(";"); + } + + CodeGen code = new CodeGen( + encoded + """ + +int 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( int 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( int d = 1; d > 0; ) { + command = program[++pc]; + if (command == 91) d++; + if (command == 93) d--; + } + } + } else if (command == 93) { + if (data[d]) { + for( int d = 1; d > 0; ) { + command = program[--pc]; + if (command == 93) d++; + if (command == 91) d--; + } + } + } +} +return output; + """); + code.parse().opto().typeCheck()/*.instSelect("x86_64_v2", "SystemV")*/.GCM().localSched(); + assertEquals("Hello World!\n", Eval2.eval(code, 0, 10000)); + } +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter01Test.java new file mode 100644 index 00000000..934eda76 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter01Test.java @@ -0,0 +1,94 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter01Test { + + @Test + public void testSimpleProgram() { + CodeGen code = new CodeGen("return 1;").parse(); + Node expr = code.expr(); + if( expr instanceof ConstantNode con ) { + assertEquals(code._start,con.in(0)); + assertEquals(TypeInteger.constant(1), con._type); + } else { + fail(); + } + } + + @Test + public void testZero() { + CodeGen code = new CodeGen("return 0;"); + code.parse(); + for( Node use : code._start._outputs ) + if( use instanceof ConstantNode con && con._type instanceof TypeInteger ) + assertEquals(TypeInteger.constant(0),con._type); + } + + @Test + public void testBad1() { + try { + new CodeGen("ret;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'ret'",e.getMessage()); + } + } + + @Test + public void testBad2() { + try { + new CodeGen("return 0123;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error: integer values cannot start with '0'",e.getMessage()); + } + } + + @Test + public void testNotBad3() { + // this test used to fail in chapter 1 + assertEquals("return 12;", new CodeGen("return - -12;").parse()._stop.print()); + } + + @Test + public void testBad4() { + try { + new CodeGen("return 100").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, expected ;: ",e.getMessage()); + } + } + + @Test + public void testNotBad5() { + // this test used to fail in chapter 1 + assertEquals("return -100;", new CodeGen("return -100;").parse()._stop.print()); + } + + @Test + public void testBad6() { + try { + new CodeGen("return100;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'return100'",e.getMessage()); + } + } + + @Test + public void testBad7() { + try { + new CodeGen("return 1;}").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, unexpected: }",e.getMessage()); + } + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter02Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter02Test.java new file mode 100644 index 00000000..d8607c32 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter02Test.java @@ -0,0 +1,60 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter02Test { + + @Test + public void testParseGrammar() { + CodeGen code = new CodeGen("return 1+2*3+-5;"); + code.parse(); // No more disable peepholes + assertEquals("return 2;", code.print()); + } + + @Test + public void testAddPeephole() { + CodeGen code = new CodeGen("return 1+2;"); + code.parse(); + assertEquals("return 3;", code.print()); + } + + @Test + public void testSubPeephole() { + CodeGen code = new CodeGen("return 1-2;"); + code.parse(); + assertEquals("return -1;", code.print()); + } + + @Test + public void testMulPeephole() { + CodeGen code = new CodeGen("return 2*3;"); + code.parse(); + assertEquals("return 6;", code.print()); + } + + @Test + public void testDivPeephole() { + CodeGen code = new CodeGen("return 6/3;"); + code.parse(); + assertEquals("return 2;", code.print()); + } + + @Test + public void testMinusPeephole() { + CodeGen code = new CodeGen("return 6/-3;"); + code.parse(); + assertEquals("return -2;", code.print()); + } + + @Test + public void testExample() { + CodeGen code = new CodeGen("return 1+2*3+-5;"); + code.parse(); + assertEquals("return 2;", code.print()); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter03Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter03Test.java new file mode 100644 index 00000000..4f9b2490 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter03Test.java @@ -0,0 +1,73 @@ +package com.seaofnodes.simple; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter03Test { + + @Test + public void testVarDecl() { + CodeGen code = new CodeGen("int a=1; return a;"); + code.parse(); assertEquals("return 1;", code.print()); + } + + @Test + public void testVarAdd() { + CodeGen code = new CodeGen("int a=1; int b=2; return a+b;"); + code.parse(); + assertEquals("return 3;", code.print()); + } + + @Test + public void testVarScope() { + CodeGen code = new CodeGen("int a=1; int b=2; int c=0; { int b=3; c=a+b; } return c;"); + code.parse(); + assertEquals("return 4;", code.print()); + } + + @Test + public void testVarScopeNoPeephole() { + CodeGen code = new CodeGen("int a=1; int b=2; int !c=0; { int b=3; c=a+b; } return c; "); + code.parse(); // Peepholes always run now + assertEquals("return 4;", code.print()); + } + + @Test + public void testVarDist() { + CodeGen code = new CodeGen("int x0=1; int y0=2; int x1=3; int y1=4; return (x0-x1)*(x0-x1) + (y0-y1)*(y0-y1); "); + code.parse(); + assertEquals("return 8;", code.print()); + } + + @Test + public void testSelfAssign() { + try { + new CodeGen("int a=a; return a;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Undefined name 'a'",e.getMessage()); + } + } + + @Test + public void testRedeclareVar() { + try { + new CodeGen("int a=1; int b=2; int c=0; int b=3; c=a+b;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Redefining name 'b'",e.getMessage()); + } + } + + @Test + public void testBad1() { + try { + new CodeGen("int a=1; int b=2; int !c=0; { int b=3; c=a+b;").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, expected }: ",e.getMessage()); + } + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter04Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter04Test.java new file mode 100644 index 00000000..d09b1748 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter04Test.java @@ -0,0 +1,130 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; +import org.junit.Ignore; + +import static org.junit.Assert.*; + +public class Chapter04Test { + + @Test + public void testPeephole() { + CodeGen code = new CodeGen("return 1+arg+2; "); + code.parse(); + assertEquals("return (arg+3);", code.print()); + } + + @Test + public void testPeephole2() { + CodeGen code = new CodeGen("return (1+arg)+2;"); + code.parse(); + assertEquals("return (arg+3);", code.print()); + } + + @Test + public void testAdd0() { + CodeGen code = new CodeGen("return 0+arg;"); + code.parse(); + assertEquals("return arg;", code.print()); + } + + @Test + public void testAddAddMul() { + CodeGen code = new CodeGen("return arg+0+arg;"); + code.parse(); + assertEquals("return (arg<<1);", code.print()); + } + + @Test + public void testPeephole3() { + CodeGen code = new CodeGen("return 1+arg+2+arg+3; "); + code.parse(); + assertEquals("return ((arg<<1)+6);", code.print()); + } + + @Test + public void testMul1() { + CodeGen code = new CodeGen("return 1*arg;"); + code.parse(); + assertEquals("return arg;", code.print()); + } + + @Test + public void testVarArg() { + CodeGen code = new CodeGen("return arg; "); + code.parse(); + assertTrue(code.ctrl() instanceof FunNode); + assertTrue(code.expr() instanceof ParmNode); + } + + // Adding functions means `main` does not constant fold the incoming arg value + @Ignore @Test + public void testConstantArg() { + CodeGen code = new CodeGen("return arg; ", TypeInteger.constant(2), 123L).parse(); + assertEquals("return 2;", code.expr().in(1).toString()); + } + + @Test + public void testCompEq() { + CodeGen code = new CodeGen("return 3==3; "); + code.parse(); + assertEquals("return 1;", code.print()); + } + + @Test + public void testCompEq2() { + CodeGen code = new CodeGen("return 3==4; "); + code.parse(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testCompNEq() { + CodeGen code = new CodeGen("return 3!=3; "); + code.parse(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testCompNEq2() { + CodeGen code = new CodeGen("return 3!=4; "); + code.parse(); + assertEquals("return 1;", code.print()); + } + + @Test + public void testBug1() { + CodeGen code = new CodeGen("int a=arg+1; int !b=a; b=1; return a+2; "); + code.parse(); + assertEquals("return (arg+3);", code.print()); + } + + @Test + public void testBug2() { + CodeGen code = new CodeGen("int !a=arg+1; a=a; return a; "); + code.parse(); + assertEquals("return (arg+1);", code.print()); + } + + @Test + public void testBug3() { + try { new CodeGen("inta=1; return a;").parse(); fail(); } + catch( RuntimeException e ) { assertEquals("Undefined name 'inta'",e.getMessage()); } + } + + @Test + public void testBug4() { + CodeGen code = new CodeGen("return -arg;"); + code.parse(); + assertEquals("return (-arg);", code.print()); + } + + @Test + public void testBug5() { + CodeGen code = new CodeGen("return arg--2;"); + code.parse(); + assertEquals("return (arg--2);", code.print()); + } +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter05Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter05Test.java new file mode 100644 index 00000000..86ec4136 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter05Test.java @@ -0,0 +1,185 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter05Test { + + @Test + public void testIfStmt() { + CodeGen code = new CodeGen( +""" +int a = 1; +if (arg == 1) + a = arg+2; +else { + a = arg-3; +} +return a; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,(arg+2),(arg-3));", code.print()); + } + + @Test + public void testTest() { + CodeGen code = new CodeGen( +""" +int c = 3; +int b = 2; +if (arg == 1) { + b = 3; + c = 4; +} +return c; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,4,3);", code.print()); + } + + @Test + public void testReturn2() { + CodeGen code = new CodeGen( +""" +if( arg==1 ) + return 3; +else + return 4; +"""); + code.parse(); + assertEquals("return Phi(Region,3,4);", code.print()); + } + + @Test + public void testIfMergeB() { + CodeGen code = new CodeGen( +""" +int a=arg+1; +int b=0; +if( arg==1 ) + b=a; +else + b=a+1; +return a+b;"""); + code.parse().opto(); + assertEquals("return ((arg<<1)+Phi(Region,2,3));", code.print()); + } + + @Test + public void testIfMerge2() { + CodeGen code = new CodeGen( +""" +int a=arg+1; +int b=arg+2; +if( arg==1 ) + b=b+a; +else + a=b+1; +return a+b;"""); + code.parse().opto(); + assertEquals("return ((Phi(Region,(arg<<1),arg)+arg)+Phi(Region,4,5));", code.print()); + } + + @Test + public void testMerge3() { + CodeGen code = new CodeGen( +""" +int a=1; +if( arg==1 ) + if( arg==2 ) + a=2; + else + a=3; +else if( arg==3 ) + a=4; +else + a=5; +return a; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,Phi(Region,2,3),Phi(Region,4,5));", code.print()); + } + + @Test + public void testMerge4() { + CodeGen code = new CodeGen( +""" +int a=0; +int b=0; +if( arg ) + a=1; +if( arg==0 ) + b=2; +return arg+a+b; +"""); + code.parse().opto(); + assertEquals("return ((arg+Phi(Region,1,0))+Phi(Region,2,0));", code.print()); + } + + @Test + public void testMerge5() { + CodeGen code = new CodeGen( +""" +int a=arg==2; +if( arg==1 ) +{ + a=arg==3; +} +return a; +"""); + code.parse().opto(); + assertEquals("return (arg==Phi(Region,3,2));", code.print()); + } + + @Test + public void testTrue() { + CodeGen code = new CodeGen("return true;").parse(); + assertEquals("return 1;",code.print()); + } + + @Test + public void testHalfDef() { + try { new CodeGen("if( arg==1 ) int b=2; return b;").parse(); fail(); } + catch( RuntimeException e ) { assertEquals("Cannot define a 'b' on one arm of an if",e.getMessage()); } + } + + @Test + public void testHalfDef2() { + try { new CodeGen("if( arg==1 ) { int b=2; } else { int b=3; } return b;").parse(); fail(); } + catch( RuntimeException e ) { assertEquals("Undefined name 'b'",e.getMessage()); } + } + + @Test + public void testRegress1() { + try { new CodeGen("if(arg==2) int a=1; else int b=2; return a;").parse(); fail(); } + catch( RuntimeException e ) { assertEquals("Cannot define a 'a' on one arm of an if",e.getMessage()); } + } + + + @Test + public void testBadNum() { + try { new CodeGen("return 1-;").parse(); fail(); } + catch( RuntimeException e ) { assertEquals("Syntax error, expected an identifier or expression: ;",e.getMessage()); } + } + + @Test + public void testKeyword1() { + try { new CodeGen("int true=0; return true;").parse(); fail(); } + catch( RuntimeException e ) { assertEquals("Expected an identifier, found 'true'",e.getMessage()); } + } + + @Test + public void testKeyword2() { + try { new CodeGen("int else=arg; if(else) else=2; else else=1; return else;").parse(); fail(); } + catch( RuntimeException e ) { assertEquals("Expected an identifier, found 'else'",e.getMessage()); } + } + + @Test + public void testKeyword3() { + try { new CodeGen("int a=1; ififif(arg);a=2;return a;").parse(); fail(); } + catch( RuntimeException e ) { assertEquals("Undefined name 'ififif'",e.getMessage()); } + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter06Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter06Test.java new file mode 100644 index 00000000..117531f0 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter06Test.java @@ -0,0 +1,304 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter06Test { + + @Test + public void testPeepholeReturn() { + CodeGen code = new CodeGen(""" +if( true ) return 2; +return 1; +"""); + code.parse().opto(); + assertEquals("return 2;", code.print()); + assertTrue(code.ctrl() instanceof FunNode); + } + + @Test + public void testPeepholeRotate() { + CodeGen code = new CodeGen( +""" +int a = 1; +if (arg) + a = 2; +return (arg < a) < 3; // Because (arg < a) is a bool/uint1/[0-1], its always less than 3 +"""); + code.parse().opto(); + assertEquals("return 1;", code.print()); + } + + @Test + public void testPeepholeCFG() { + CodeGen code = new CodeGen( +""" +int a=1; +if( true ) + a=2; +else + a=3; +return a; +"""); + code.parse().opto(); + assertEquals("return 2;", code.print()); + assertTrue(code.ctrl() instanceof FunNode); + } + + @Test + public void testIfIf() { + CodeGen code = new CodeGen( +""" +int a=1; +if( arg!=1 ) + a=2; +else + a=3; +int b=4; +if( a==2 ) + b=42; +else + b=5; +return b; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,42,5);", code.print()); + } + + @Test + public void testIfArgIf() { + CodeGen code = new CodeGen( +""" +int a=1; +if( 1==1 ) + a=2; +else + a=3; +int b=4; +if( arg==2 ) + b=a; +else + b=5; +return b;"""); + code.parse().opto(); + assertEquals("return Phi(Region,2,5);", code.print()); + } + + @Test + public void testMerge3With2() { + CodeGen code = new CodeGen( +""" +arg=2; +int a=1; +if( arg==1 ) + if( arg==2 ) + a=2; + else + a=3; +else if( arg==3 ) + a=4; +else + a=5; +return a; +""", TypeInteger.constant(2), 123L).parse().opto(); + assertEquals("return 5;", code.print()); + } + + @Test + public void testMerge3With1() { + CodeGen code = new CodeGen( +""" +arg=1; +int a=1; +if( arg==1 ) + if( arg==2 ) + a=2; + else + a=3; +else if( arg==3 ) + a=4; +else + a=5; +return a; +""", TypeInteger.constant(1), 123L).parse().opto(); + assertEquals("return 3;", code.print()); + } + + @Test + public void testMerge3Peephole() { + CodeGen code = new CodeGen( +""" +int a=1; +if( arg==1 ) + if( 1==2 ) + a=2; + else + a=3; +else if( arg==3 ) + a=4; +else + a=5; +return a; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,3,Phi(Region,4,5));", code.print()); + } + + @Test + public void testMerge3Peephole1() { + CodeGen code = new CodeGen( +""" +arg=1; +int a=1; +if( arg==1 ) + if( 1==2 ) + a=2; + else + a=3; +else if( arg==3 ) + a=4; +else + a=5; +return a; +""", TypeInteger.constant(1), 123L); + code.parse().opto(); + assertEquals("return 3;", code.print()); + } + + @Test + public void testMerge3Peephole3() { + CodeGen code = new CodeGen( +""" +arg=3; +int a=1; +if( arg==1 ) + if( 1==2 ) + a=2; + else + a=3; +else if( arg==3 ) + a=4; +else + a=5; +return a; +""", TypeInteger.constant(3), 123L); + code.parse().opto(); + assertEquals("return 4;", code.print()); + } + + @Test + public void testDemo1NonConst() { + CodeGen code = new CodeGen(""" +int a = 0; +int b = 1; +if( arg ) { + a = 2; + if( arg ) { b = 2; } + else b = 3; +} +return a+b; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,4,1);", code.print()); + } + + + @Test + public void testDemo1True() { + CodeGen code = new CodeGen( +""" +arg=1; +int a = 0; +int b = 1; +if( arg ) { + a = 2; + if( arg ) { b = 2; } + else b = 3; +} +return a+b; +""", TypeInteger.constant(1), 123L); + code.parse().opto(); + assertEquals("return 4;", code.print()); + } + + @Test + public void testDemo1False() { + CodeGen code = new CodeGen( +""" +arg=0; +int a = 0; +int b = 1; +if( arg ) { + a = 2; + if( arg ) { b = 2; } + else b = 3; +} +return a+b; +""", TypeInteger.constant(0), 123L); + code.parse().opto(); + assertEquals("return 1;", code.print()); + } + + @Test + public void testDemo2NonConst() { + CodeGen code = new CodeGen(""" +int a = 0; +int b = 1; +int c = 0; +if( arg ) { + a = 1; + if( arg==2 ) { c=2; } else { c=3; } + if( arg ) { b = 2; } + else b = 3; +} +return a+b+c; +"""); + code.parse().opto(); + assertEquals("return (Phi(Region,Phi(Region,2,3),0)+Phi(Region,3,1));", code.print()); + } + + + @Test + public void testDemo2True() { + CodeGen code = new CodeGen( +""" +arg=1; +int a = 0; +int b = 1; +int c = 0; +if( arg ) { + a = 1; + if( arg==2 ) { c=2; } else { c=3; } + if( arg ) { b = 2; } + else b = 3; +} +return a+b+c; +""", TypeInteger.constant(1), 123L); + code.parse().opto(); + assertEquals("return 6;", code.print()); + } + + @Test + public void testDemo2arg2() { + CodeGen code = new CodeGen( +""" +arg=2; +int a = 0; +int b = 1; +int c = 0; +if( arg ) { + a = 1; + if( arg==2 ) { c=2; } else { c=3; } + if( arg ) { b = 2; } + else b = 3; +} +return a+b+c; +""", TypeInteger.constant(2), 123L); + code.parse().opto(); + assertEquals("return 5;", code.print()); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter07Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter07Test.java new file mode 100644 index 00000000..6d6a3ce7 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter07Test.java @@ -0,0 +1,221 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter07Test { + + @Test + public void testExample() { + CodeGen code = new CodeGen( +""" +while(arg < 10) { + arg = arg + 1; +} +return arg; +"""); + code.parse(); + assertEquals("return Phi(Loop,arg,(Phi_arg+1));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + @Test + public void testRegression() { + CodeGen code = new CodeGen( +""" +int a = 1; +if(arg){}else{ + while(a < 10) { + a = a + 1; + } +} +return a; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,1,Phi(Loop,1,(Phi_a+1)));", code.print()); + } + + @Test + public void testWhileNested() { + CodeGen code = new CodeGen( +""" +int sum = 0; +int i = 0; +while(i < arg) { + i = i + 1; + int j = 0; + while( j < arg ) { + sum = sum + j; + j = j + 1; + } +} +return sum; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,0,Phi(Loop,Phi_sum,(Phi_sum+Phi(Loop,0,(Phi_j+1)))));", code.print()); + } + + @Test + public void testWhileScope() { + CodeGen code = new CodeGen( +""" +int a = 1; +int b = 2; +while(a < 10) { + if (a == 2) a = 3; + else b = 4; +} +return b; +"""); + code.parse(); + assertEquals("return Phi(Loop,2,Phi(Region,Phi_b,4));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + @Test + public void testWhileNestedIfAndInc() { + CodeGen code = new CodeGen( +""" +int a = 1; +int b = 2; +while(a < 10) { + if (a == 2) a = 3; + else b = 4; + b = b + 1; + a = a + 1; +} +return b; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,2,(Phi(Region,Phi_b,4)+1));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + + @Test + public void testWhile() { + CodeGen code = new CodeGen( +""" +int a = 1; +while(a < 10) { + a = a + 1; + a = a + 2; +} +return a; +"""); + code.parse(); + assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + @Test + public void testWhilePeep() { + CodeGen code = new CodeGen( +""" +int a = 1; +while(a < 10) { + a = a + 1; + a = a + 2; +} +return a; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + @Test + public void testWhile2() { + CodeGen code = new CodeGen( +""" +int a = 1; +while(arg) a = 2; +return a; +"""); + code.parse(); + assertEquals("return Phi(Loop,1,2);", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + @Test + public void testWhile2Peep() { + CodeGen code = new CodeGen( +""" +int a = 1; +while(arg) a = 2; +return a; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,1,2);", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + @Test + public void testWhile3() { + CodeGen code = new CodeGen( +""" +int a = 1; +while(a < 10) { + int b = a + 1; + a = b + 2; +} +return a; +"""); + code.parse(); + assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + @Test + public void testWhile3Peep() { + CodeGen code = new CodeGen( +""" +int a = 1; +while(a < 10) { + int b = a + 1; + a = b + 2; +} +return a; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + @Test + public void testWhile4() { + CodeGen code = new CodeGen( +""" +int a = 1; +int b = 2; +while(a < 10) { + int b = a + 1; + a = b + 2; +} +return a; +"""); + code.parse(); + assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + @Test + public void testWhile4Peep() { + CodeGen code = new CodeGen( +""" +int a = 1; +int b = 2; +while(a < 10) { + int b = a + 1; + a = b + 2; +} +return a; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter08Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter08Test.java new file mode 100644 index 00000000..350df18b --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter08Test.java @@ -0,0 +1,207 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import org.junit.Assert; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter08Test { + + @Test + public void testEx6() { + CodeGen code = new CodeGen( + """ +while(arg < 10) { + arg = arg + 1; + if (arg == 5) + break; + if (arg == 6) + break; +} +return arg; + """); + code.parse().opto(); + assertEquals("return Phi(Region,Phi(Region,Phi(Loop,arg,(Phi_arg+1)),Add),Add);", code.print()); + assertTrue(code.ctrl() instanceof RegionNode); + Assert.assertEquals( "5", Eval2.eval(code, 1)); + Assert.assertEquals("10", Eval2.eval(code, 6)); + } + + + @Test + public void testEx5() { + CodeGen code = new CodeGen( + """ +int a = 1; +while(arg < 10) { + arg = arg + 1; + if (arg == 5) + continue; + if (arg == 7) + continue; + a = a + 1; +} +return a; + """); + code.parse().opto(); + assertEquals("return Phi(Loop,1,Phi(Region,Phi_a,(Phi_a+1)));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + + @Test + public void testEx4() { + CodeGen code = new CodeGen( + """ +while(arg < 10) { + arg = arg + 1; + if (arg == 5) + continue; + if (arg == 6) + break; +} +return arg; + """); + code.parse().opto(); + assertEquals("return Phi(Region,Phi(Loop,arg,(Phi_arg+1)),Add);", code.print()); + assertTrue(code.ctrl() instanceof RegionNode); + } + + @Test + public void testEx3() { + CodeGen code = new CodeGen( + """ +while(arg < 10) { + arg = arg + 1; + if (arg == 6) + break; +} +return arg; + """); + code.parse().opto(); + assertEquals("return Phi(Region,Phi(Loop,arg,(Phi_arg+1)),Add);", code.print()); + assertTrue(code.ctrl() instanceof RegionNode); + } + + + @Test + public void testEx2() { + CodeGen code = new CodeGen( + """ +while(arg < 10) { + arg = arg + 1; + if (arg == 5) + continue; + if (arg == 6) + continue; +} +return arg; + """); + code.parse().opto(); + assertEquals("return Phi(Loop,arg,(Phi_arg+1));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + + @Test + public void testEx1() { + CodeGen code = new CodeGen( + """ +while(arg < 10) { + arg = arg + 1; + if (arg == 5) + continue; +} +return arg; + """); + code.parse().opto(); + assertEquals("return Phi(Loop,arg,(Phi_arg+1));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + + @Test + public void testRegress1() { + CodeGen code = new CodeGen( + """ +while( arg < 10 ) { + int a = arg+2; + if( a > 4 ) + break; +} +return arg; + """); + code.parse().opto(); + assertEquals("return arg;", code.print()); + } + + @Test + public void testRegress2() { + CodeGen code = new CodeGen("if(1) return 0; else while(arg>- -arg) arg=arg+1; return 0;"); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testBreakOutsideLoop() { + try { + new CodeGen(""" +if(arg <= 10) { + break; + arg = arg + 1; +} +return arg; +""").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("No active loop for a break or continue",e.getMessage()); + } + } + + @Test + public void testRegress3() { + CodeGen code = new CodeGen(""" +while(arg < 10) { + break; +} +return arg; +"""); + code.parse().opto(); + assertEquals("return arg;", code.print()); + } + + @Test + public void testRegress4() { + CodeGen code = new CodeGen(""" +int a = 1; +while(arg < 10) { + a = a + 1; + if (arg > 2) { + int a = 17; + break; + } +} +return a; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,Phi(Loop,1,(Phi_a+1)),Add);", code.print()); + assertTrue(code.ctrl() instanceof RegionNode); + } + + @Test + public void testRegress5() { + CodeGen code = new CodeGen(""" +int a = 1; +while(1) { + a = a + 1; + if (a<10) continue; + break; +} +return a; +"""); + code.parse().opto(); + assertEquals("return (Phi(Loop,1,Add)+1);", code.print()); + assertTrue(code.ctrl() instanceof CProjNode prj && prj._idx==1 ); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter09Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter09Test.java new file mode 100644 index 00000000..10f4a78b --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter09Test.java @@ -0,0 +1,296 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.evaluator.Evaluator; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Assert; +import org.junit.Test; +import org.junit.Ignore; + +import static org.junit.Assert.*; + +public class Chapter09Test { + + @Test + @Ignore + public void testJig() { + CodeGen code = new CodeGen(""" +int v0=0; +arg=0; +while(v0) { + while(1) if(arg*arg*0==0) {} + while(0) {} + arg=1; +} +return 0; + """); + code.parse().opto(); + } + + @Test + public void testGVN1() { + CodeGen code = new CodeGen( + """ +int x = arg + arg; +if(arg < 10) { + return arg + arg; +} +else { + x = x + 1; +} +return x; + """); + code.parse().opto(); + assertEquals("return Phi(Region,(arg<<1),(Shl+1));", code.print()); + Assert.assertEquals( "2", Eval2.eval(code, 1)); + Assert.assertEquals("23", Eval2.eval(code, 11)); + } + + @Test + public void testGVN2() { + CodeGen code = new CodeGen("return arg*arg-arg*arg;"); + code.parse().opto(); + assertEquals("return 0;", code.print()); + Assert.assertEquals("0", Eval2.eval(code, 1, 1)); + } + + @Test + public void testWorklist1() { + CodeGen code = new CodeGen(""" +int step = 1; +while (arg < 10) { + arg = arg + step + 1; +} +return arg; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,arg,(Phi_arg+2));", code.print()); + Assert.assertEquals("11", Eval2.eval(code, 1)); + } + + @Test + public void testWorklist2() { + CodeGen code = new CodeGen( + """ +int cond = 0; +int one = 1; +while (arg < 10) { + if (cond) one = 2; + arg = arg + one*3 + 1; +} +return arg; + """); + code.parse().opto(); + assertEquals("return Phi(Loop,arg,(Phi_arg+4));", code.print()); + Assert.assertEquals("13", Eval2.eval(code, 1)); + } + + @Test + public void testWorklist3() { + CodeGen code = new CodeGen( + """ +int v1 = 0; +int v2 = 0; +int v3 = 0; +int v4 = 0; +int v5 = 0; +int v6 = 0; +int v7 = 0; +int v8 = 0; +while (arg) { + if (v1) v2 = 1; + if (v2) v3 = 1; + if (v3) v4 = 1; + if (v4) v5 = 1; + if (v5) v6 = 1; + if (v6) v7 = 1; + if (v7) v8 = 1; + arg = arg + v8 + 1; +} +return arg; + """); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testRegionPeepBug() { + CodeGen code = new CodeGen( + """ +int v0=0; +int v1=0; +while(v1+arg) { + arg=0; + int v2=v0; + while(arg+1) {} + v0=1; + v1=v2; +} + """); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testWhile0() { + CodeGen code = new CodeGen("while(0) continue; if(0) arg=0;"); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testWhile1() { + CodeGen code = new CodeGen(""" +if(0) while(0) { + int arg=arg; + while(0) {} +} +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + + @Test + public void testPrecedence() { + CodeGen code = new CodeGen("return 3-1+2;"); + code.parse().opto(); + assertEquals("return 4;", code.print()); + } + + @Test + public void testSwap2() { + CodeGen code = new CodeGen("return 1+(1+1);"); + code.parse().opto(); + assertEquals("return 3;", code.print()); + } + + + @Test + public void testFuzz0() { + CodeGen code = new CodeGen(""" +int one = 1; +int a = 0; +int zero = 0; +while(arg) { + a = -(one + a + 2); + arg = arg + 1; + one = one + zero; +} +return a; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,0,(-(Phi_a+3)));", code.print()); + } + + @Test + public void testFuzz1() { + CodeGen code = new CodeGen(""" +while(1) {} +while(arg) break; +while(arg) arg=0; +arg=0; +int v0=0!=0<-0; +return -0+0+0; +"""); + code.parse().opto(); + assertEquals("return Top;", code.print()); + } + + + @Test + public void testFuzz2() { + CodeGen code = new CodeGen("return 0+-0;"); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testFuzz3() { + CodeGen code = new CodeGen("int v0=0; while(0==69) while(v0) return 0;"); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testFuzz4() { + CodeGen code = new CodeGen(""" +while(1) { + arg=0<=0; + if(1<0) while(arg==-0) arg=arg-arg; +} +"""); + code.parse().opto(); + assertEquals("return Top;", code.print()); + } + + @Test + public void testFuzz5() { + CodeGen code = new CodeGen(""" +{ + int v0=0; + while(1) + int v1=0--0; + while(v0) + break; + while(-v0) { + while(0+0+v0) continue; + break; + } + if(-0!=-0+0+v0) while(0+0+0+0) + break; +} +return 0!=0; +"""); + code.parse().opto(); + assertEquals("return Top;", code.print()); + } + + @Test + public void testFuzz6() { + CodeGen code = new CodeGen( +""" +int v0=0; +while(0==1) while(v0) + v0=1+v0; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testFuzz7() { + CodeGen code = new CodeGen(""" +while(1) {} +int v0=0; +while(v0) + {} +int v1=0; +while(1) + v1=1; +return v1+v0; + """); + code.parse().opto(); + assertEquals("return Top;", code.print()); + } + + @Test + public void testFuzz8() { + CodeGen code = new CodeGen("while(arg) arg = arg - 1; return arg;"); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testMeet() { + Type t1 = Type.TOP; + Type t2 = TypeInteger.TOP; + Assert.assertEquals(TypeInteger.TOP, t1.meet(t2)); + Assert.assertEquals(TypeInteger.TOP, t2.meet(t1)); + t1 = Type.BOTTOM; + t2 = TypeInteger.BOT; + Assert.assertEquals(Type.BOTTOM, t1.meet(t2)); + Assert.assertEquals(Type.BOTTOM, t2.meet(t1)); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter10Test.java new file mode 100644 index 00000000..02029b3f --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter10Test.java @@ -0,0 +1,396 @@ +package com.seaofnodes.simple; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter10Test { + + @Test + public void testFuzzer() { + CodeGen code = new CodeGen( +""" +int a = arg/3; +int b = arg*5; +int x = arg*7; +int y = arg/11; +int p; int g; int h; +if( (arg/13)==0 ) { + p = x + y; + g = x; + h = y; +} else { + p = a + b; + g = a; + h = b; +} +int r = g+h; +return p-r; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testStruct() { + CodeGen code = new CodeGen(""" +struct Bar { + int a; + int b; +}; +struct Foo { + int x; +}; +Foo? foo = null; +Bar !bar = new Bar; +bar.a = 1; +bar.a = 2; +return bar.a; +"""); + code.parse().opto(); + assertEquals("return 2;", code.print()); + } + + @Test + public void testExample() { + CodeGen code = new CodeGen(""" +struct Vector2D { int x; int y; }; +Vector2D !v = new Vector2D; +v.x = 1; +if (arg) + v.y = 2; +else + v.y = 3; +return v; +"""); + code.parse().opto(); + assertEquals("return Vector2D;", code.print()); + } + + @Test + public void testBug() { + CodeGen code = new CodeGen(""" +struct s0 { + int v0; +}; +s0? v1=null; +int v3=v1.zAicm; +"""); + try { code.parse(); fail(); } + catch( Exception e ) { assertEquals("Accessing unknown field 'zAicm' from 'null'",e.getMessage()); } + } + + @Test + public void testBug2() { + CodeGen code = new CodeGen(""" +struct s0 { int v0; }; +arg=0+new s0.0; +"""); + try { code.parse(); fail(); } + catch( Exception e ) { assertEquals("Expected an identifier, found 'null'",e.getMessage()); } + } + + @Test + public void testLoop() { + CodeGen code = new CodeGen(""" +struct Bar { int a; }; +Bar !bar = new Bar; +while (arg) { + bar.a = bar.a + 2; + arg = arg + 1; +} +return bar.a; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,0,(Phi_a+2));", code.print()); + } + + @Test + public void testIf() { + CodeGen code = new CodeGen(""" +struct Bar { int a; }; +Bar !bar = new Bar; +if (arg) bar = null; +bar.a = 1; +return bar.a; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Type null is not of declared type *Bar",e.getMessage()); } + } + + @Test + public void testIf2() { + CodeGen code = new CodeGen(""" +struct Bar { int a; }; +Bar? !bar = null; +if (arg) bar = new Bar; +bar.a = 1; +return bar.a; +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Might be null accessing 'a'",e.getMessage()); } + } + + @Test + public void testIf3() { + CodeGen code = new CodeGen(""" +struct Bar { int a; }; +Bar bar = null; +if (arg) bar = null; +bar.a = 1; +return bar.a; +"""); + try { code.parse(); fail(); } + catch( Exception e ) { assertEquals("Type null is not of declared type *Bar", e.getMessage()); } + } + + @Test + public void testIfOrNull() { + CodeGen code = new CodeGen(""" +struct Bar { int a; }; +Bar? !bar = new Bar; +if (arg) bar = null; +if( bar ) bar.a = 1; +return bar; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,null,Bar);", code.print()); + } + + @Test + public void testIfOrNull2() { + CodeGen code = new CodeGen( +""" +struct Bar { int a; }; +Bar? !bar = new Bar; +if (arg) bar = null; +int rez = 3; +if( !bar ) rez=4; +else bar.a = 1; +return rez; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,4,3);", code.print()); + } + + @Test + public void testWhileWithNullInside() { + CodeGen code = new CodeGen(""" +struct s0 {int v0;}; +s0? !v0 = new s0; +int ret = 0; +while(arg) { + ret = v0.v0; + v0 = null; + arg = arg - 1; +} +return ret; +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { + assertEquals("Might be null accessing 'v0'", e.getMessage()); } + } + + @Test + public void testRedeclareStruct() { + CodeGen code = new CodeGen(""" +struct s0 { + int v0; +}; +s0? v1=new s0; +s0? v1; +v1=new s0; +"""); + try { code.parse(); fail(); } + catch( Exception e ) { assertEquals("Redefining name 'v1'", e.getMessage()); } + } + + @Test + public void testIter() { + // Build and use an iterator + CodeGen code = new CodeGen( +""" +struct Iter { + int x; + int len; +}; +Iter !i = new Iter; +i.len = arg; +int sum=0; +while( i.x < i.len ) { + sum = sum + i.x; + i.x = i.x + 1; +} +return sum; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,0,(Phi(Loop,0,(Phi_x+1))+Phi_sum));", code.print()); + } + + + @Test + public void test1() { + CodeGen code = new CodeGen(""" +struct s0 {int v0;}; +s0 !ret = new s0; +while(arg) { + s0 !v0 = new s0; + v0.v0 = arg; + arg = arg-1; + if (arg==5) ret=v0; + +} +return ret; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,s0,Phi(Region,s0,Phi_ret));", code.print()); + } + + @Test + public void test2() { + CodeGen code = new CodeGen(""" +struct s0 {int v0;}; +s0 !ret = new s0; +s0 !v0 = new s0; +while(arg) { + v0.v0 = arg; + arg = arg-1; + if (arg==5) ret=v0; + +} +return ret; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,s0,Phi(Region,s0,Phi_ret));", code.print()); + } + + + @Test + public void test3() { + CodeGen code = new CodeGen(""" +struct s0 {int v0;}; +s0 !ret = new s0; +while(arg < 10) { + s0 !v0 = new s0; + if (arg == 5) ret=v0; + arg = arg + 1; +} +return ret; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,s0,Phi(Region,s0,Phi_ret));", code.print()); + } + + @Test + public void testBug3() { + CodeGen code = new CodeGen(""" +struct s0 { int f0; }; +return new s0; +int v0=null.f0; +"""); + try { code.parse(); fail(); } + catch( Exception e ) { assertEquals("Accessing unknown field 'f0' from 'null'",e.getMessage()); } + } + + @Test + public void testBug4() { + CodeGen code = new CodeGen(""" +if(0) { + while(0) if(arg) continue; + int v0=0; + while(1) { + int v2=-arg; + v0=v2; + } +} + """); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testBug5() { + CodeGen code = new CodeGen(""" +struct s0 { + int f0; +}; +if(0) return 0; +else return new s0; +if(new s0.f0) return 0; + """); + code.parse().opto(); + assertEquals("return s0;", code.print()); + } + + @Test + public void testBug6MissedWorklist() { + CodeGen code = new CodeGen(""" +while(0) {} +int v4=0; +while(0<arg) { + v4=v4+1; + while(1) v4=-v4; + while(0) arg=-1; +} +return 0; + """); + code.parse().opto(); + } + + @Test + public void testBug7() { + CodeGen code = new CodeGen(""" +struct s0 { int f0; }; +s0 v0 = new s0; +while(v0.f0) {} +s0 v1 = v0; +return v1; + """); + code.parse().opto(); + assertEquals("return (const)s0;", code.print()); + } + + + @Test + public void testBug8() { + CodeGen code = new CodeGen(""" +int v2=0; +while(0) +while(0) {} +{ + { + { + int v36=0; + { + while(0) { + { + while(-v2) { + { + while(v36) { + while(v2) return 0; + break; + } } + if(-v2) break; + } + } + } + } + } } +} +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testBug9() { + CodeGen code = new CodeGen(""" +int v0=arg==0; +while(v0) continue; +return 0; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter11Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter11Test.java new file mode 100644 index 00000000..0b24bd5d --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter11Test.java @@ -0,0 +1,552 @@ +package com.seaofnodes.simple; + +import org.junit.Test; + + +import static org.junit.Assert.assertEquals; + +public class Chapter11Test { + + // A placeholder test used to rapidly rotate through fuzzer produced issues + @Test + public void testFuzzer() { + CodeGen code = new CodeGen( +""" +while(---- -arg) { + while(-arg) { + while(-----arg*---0) { + int N=--false==false<--arg; + while(N<-4==N) { + N=N+N; + } + } + { + int kzEl_r7EYz=3*----0!=-arg<(37*--97!=0/arg==arg)<=-(--arg!=0); + while(kzEl_r7EYz<---1) { + kzEl_r7EYz=kzEl_r7EYz+kzEl_r7EYz*arg>=24; + if(7) continue; + kzEl_r7EYz=false; + if(-kzEl_r7EYz+arg<-3) continue; + else { + while(kzEl_r7EYz) continue; + arg=----3; + kzEl_r7EYz=-(-----arg*1>-kzEl_r7EYz); + int v2uF_4ba=9; + } + } + } + int pjcp7rv=arg; + { + int m2ya3=--arg-pjcp7rv; + while(m2ya3<-----pjcp7rv>=pjcp7rv+--pjcp7rv!=---pjcp7rv!=-arg<---(-m2ya3)>m2ya3<=-----true>14!=--m2ya3) { + m2ya3=m2ya3+-(--0<=false<pjcp7rv/arg<=-true); + while((--m2ya3)<--arg<-----arg*false!=arg<pjcp7rv*arg) { + pjcp7rv=-3+-arg; + break; + } + while(-3) { + while(67+0-61) m2ya3=m2ya3; + if(----pjcp7rv==---(---arg)) pjcp7rv=0!=-23; + else pjcp7rv=----89; + while(m2ya3!=m2ya3<pjcp7rv) m2ya3=1; + while(--arg<=pjcp7rv) pjcp7rv=-32<=-m2ya3+62; + } + break; + } + } + { + int arg=-1<-9!=(-arg>=-pjcp7rv>=arg<=arg); + while(arg<-arg) { + arg=arg+--arg; + { + while((--93)--pjcp7rv*-pjcp7rv+arg) arg=0-(6>=(--(---41/(0))!=--pjcp7rv)>=---2*33); + pjcp7rv=11; + int c=pjcp7rv; + if(--107) break; + else break; + } + } + } + pjcp7rv=pjcp7rv; + int aR0Wkua=-pjcp7rv==pjcp7rv; + { + if(-0!=-0) while(-true) break; + else { + while(aR0Wkua) pjcp7rv=-0/pjcp7rv; + pjcp7rv=pjcp7rv; + } + } + } + if(-arg) { + { + int Isdlt0fr91=0!=arg; + while(Isdlt0fr91<-Isdlt0fr91==-Isdlt0fr91+true) { + Isdlt0fr91=Isdlt0fr91+-1!=true; + if(-arg) if(--arg<=Isdlt0fr91) Isdlt0fr91=-Isdlt0fr91; + int i=---arg<---arg!=--40; + { + int jVX43Ln4Dm=-(6)==-Isdlt0fr91/----0; + while(jVX43Ln4Dm<--(3-3)) { + jVX43Ln4Dm=jVX43Ln4Dm+-0<25; + i=24<--(jVX43Ln4Dm); + } + } + i=(arg!=----false>=7); + int Te=5>-arg; + break; + } + } + arg=arg!=--true/arg; + { + int UX=arg; + while(UX<-3*arg) { + UX=UX+arg/-1; + arg=-3--(-UX- -47<arg==--arg==arg)/arg- -UX>=-arg; + if(---12) while(-UX) break; + break; + } + } + continue; + } + if(arg) arg=--0; + else if(1) continue; + int DtTlhjYq=19==arg; + { + arg=--DtTlhjYq<=(DtTlhjYq/---DtTlhjYq<=-DtTlhjYq); + if(---arg) continue; + else { + int Zsl=40; + while(Zsl<--Zsl--false>((Zsl))==Zsl!=--DtTlhjYq-DtTlhjYq-DtTlhjYq) { + Zsl=Zsl+arg; + } + } + if((arg)) if(-DtTlhjYq) { + } + else while(((2)==3*arg/DtTlhjYq)<=0==---arg) return false; + else { + int lwbF=arg>=0; + while(lwbF<57) { + lwbF=lwbF+--(-arg+lwbF)>=-2>=arg!=arg; + arg=----0+0; + while(49) break; + if(true) DtTlhjYq=-----arg; + DtTlhjYq=0!=-----(0); + } + } + arg=---arg!=DtTlhjYq/--0; + DtTlhjYq=-DtTlhjYq/(--arg<arg); + if(32) { + int q=-DtTlhjYq; + while(q<arg*(0*q)/0) { + q=q+DtTlhjYq; + DtTlhjYq=arg; + } + } + else if(--arg) if(15) break; + else arg=0==arg<=-55>DtTlhjYq; + else { + } + DtTlhjYq=-0; + { + while((0)<-arg) DtTlhjYq=-DtTlhjYq; + } + if(30/--(arg)>=0) DtTlhjYq=-0>=arg; + else { + int Y2wcIQJ=--DtTlhjYq*---11; + if(arg==arg) { + Y2wcIQJ=-----Y2wcIQJ; + Y2wcIQJ=Y2wcIQJ<7; + if(DtTlhjYq) arg=arg; + else break; + while(-arg!=-31==(false<=-15>=-DtTlhjYq+--6*-----Y2wcIQJ<=---arg!=-Y2wcIQJ<=---0<-arg*--Y2wcIQJ)) DtTlhjYq=arg!=3; + } + continue; + } + arg=DtTlhjYq; + arg=--3/(---arg)>=80>=-DtTlhjYq<=DtTlhjYq; + arg=51+---(-true)!=arg!=----1==-DtTlhjYq; + if(11<=arg) if((arg)+79<32+(-arg)) continue; + else if(true==DtTlhjYq) break; + break; + } +} +if(arg!=62==arg) { + while(---arg) { + { + int bhmDacPM=arg/arg; + while(bhmDacPM<arg) { + bhmDacPM=bhmDacPM+80*((-bhmDacPM)/---bhmDacPM>1)<bhmDacPM-2!=7; + arg=-0; + arg=bhmDacPM>=-arg+-arg; + break; + } + } + int arg=true; + arg=arg<=arg; + { + int KlC=30; + while(KlC<-2) { + KlC=KlC+-1!=25<=-KlC>KlC>----0; + int Ij=arg>=-arg<=---10; + { + int LAbgurVa2R=-arg!=Ij; + while(LAbgurVa2R<-true) { + LAbgurVa2R=LAbgurVa2R+Ij; + KlC=-LAbgurVa2R; + Ij=42*(-Ij>true); + while(Ij) arg=KlC; + KlC=-----7>-(--KlC<--7)==-LAbgurVa2R; + } + } + int a=Ij<=-KlC; + if(Ij) { + int r1W=a!=-false; + } + else { + arg=56>=Ij; + while(54) break; + arg=8>=-(a<--Ij)/a>=-a-KlC; + } + int Yt_T=KlC; + arg=KlC; + } + } + if(----false/--arg/12+-----0) if(-9+-arg) while(arg!=(arg)) arg=--1>=-71-arg!=---(--arg*11+102)/--arg; + else if(----arg) continue; + if(-arg-arg) { + arg=-(-arg)*--(-3)<(-7==-arg----arg); + { + int arg=arg; + while(arg<-0>arg==-arg-(-arg>1+11)) { + arg=arg+6-arg!=--0>---30>=---arg<=arg/(3)+---arg; + } + } + arg=-arg+arg; + arg=--47+-arg<1>arg*(-arg==-arg- -0>3); + arg=(----arg)!=arg+--59; + arg=- -4+arg==-(arg)-arg; + } + else { + while(--true-----true*-25+-18>arg>=-----(-arg)---13) { + arg=-0+--true<false; + continue; + } + arg=true<=arg; + break; + } + arg=arg*22<--(0+-arg); + { + continue; + } + } + arg=1*---((3<=true)==---2); + arg=7+-(-1); + return arg+--true; +} +else { + int o3ypEsewxM=-(---arg); + int vzXB=(-(---11<=--o3ypEsewxM>=--0))<=-0/-arg; + int L=-arg; + int B4mHcrf=---vzXB; + int oZWGdzZl=-o3ypEsewxM; + int K6tQqa1=11; + while(0!=--L) { + int sdZfTM=vzXB*true+K6tQqa1; + while(sdZfTM<false) { + sdZfTM=sdZfTM+arg; + sdZfTM=oZWGdzZl; + K6tQqa1=-K6tQqa1; + arg=---K6tQqa1; + while((3+-K6tQqa1)<oZWGdzZl) { + if(-----(-1)) sdZfTM=-false; + {} + } + K6tQqa1=vzXB!=-(B4mHcrf+-7>false); + sdZfTM=vzXB; + } + } + L=false; + while(31) B4mHcrf=arg; + oZWGdzZl=-0<--oZWGdzZl<=--2; + if(K6tQqa1>=20<=46) { + if(-1+-L) if(oZWGdzZl) { + int wmhv=-----0; + } + int arg=0; + o3ypEsewxM=---(oZWGdzZl); + return -oZWGdzZl/0; + } + else if(arg) { + int yKk5=B4mHcrf>=4>K6tQqa1<-B4mHcrf/o3ypEsewxM; + return -K6tQqa1; + } + else { + if(---0) { + {} + {} + } + return o3ypEsewxM; + } +} +"""); + code.parse().opto().typeCheck().GCM(); + assertEquals("return Phi(Region,9,Top,Top,Top,Top);", code.print()); + } + + + @Test + public void testPrimes() { + CodeGen code = new CodeGen( +""" +if( arg < 2 ) return 0; +int primeCount = 1; +int prime = 3; +while( prime <= arg ) { + int isPrime = 1; + // Check for even case, so the next loop need only check odds + if( (prime/2)*2 == prime ) + continue; + // Check odds up to sqrt of prime + int j = 3; + while( j*j <= prime ) { + if( (prime/j)*j == prime ) { + isPrime = 0; + break; + } + j = j + 2; + } + if( isPrime ) + primeCount = primeCount + 1; + prime = prime + 2; +} +return primeCount; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,0,Phi(Loop,1,Phi(Region,Phi_primeCount,Phi(Region,(Phi_primeCount+1),Phi_primeCount))));", code.print()); + assertEquals("0", Eval2.eval(code, 1)); // No primes 1 or below + assertEquals("1", Eval2.eval(code, 2)); // 2 + assertEquals("2", Eval2.eval(code, 3)); // 2, 3 + assertEquals("2", Eval2.eval(code, 4)); // 2, 3 + assertEquals("3", Eval2.eval(code, 5)); // 2, 3, 5 + assertEquals("4", Eval2.eval(code, 10)); // 2, 3, 5, 7 + } + + @Test + public void testAntiDeps1() { + CodeGen code = new CodeGen( +""" +struct S { int f; }; +S !v=new S; +v.f = 2; +int i=new S.f; +i=v.f; +if (arg) v.f=1; +return i; +"""); + code.parse().opto(); + assertEquals("return 2;", code.print()); + assertEquals("2", Eval2.eval(code, 0)); + assertEquals("2", Eval2.eval(code, 1)); + } + + @Test + public void testAntiDeps2() { + CodeGen code = new CodeGen( +""" +struct S { int f; }; +S !v = new S; +v.f = arg; +S !t = new S; +int i = 0; +if (arg) { + if (arg+1) v = t; + i = v.f; +} else { + v.f = 2; +} +return i; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,.f,0);", code.print()); + } + + @Test + public void testAntiDeps3() { + CodeGen code = new CodeGen( +""" +struct S { int f; }; +S !v0 = new S; +S? v1; +if (arg) v1 = new S; +if (v1) { + v0.f = v1.f; +} else { + v0.f = 2; +} +return v0; +"""); + code.parse().opto(); + assertEquals("return S;", code.print()); + } + + + @Test + public void testAntiDeps4() { + CodeGen code = new CodeGen( +""" +struct S { int f; }; +S !v = new S; +v.f = arg; +S t = new S; +int i = v.f; +if (arg+1) arg= 0; +while (arg) v.f = 2; +return i; +"""); + code.parse().opto(); + assertEquals("return arg;", code.print()); + } + + @Test + public void testAntiDeps5() { + CodeGen code = new CodeGen( +""" +struct S { int f; }; +S !v = new S; +while(1) { + while(arg+1) { arg=arg-1; } + if (arg) break; + v.f = 2; +} +return v; +"""); + code.parse().opto(); + assertEquals("return S;", code.print()); + } + + @Test + public void testAntiDeps6() { + CodeGen code = new CodeGen( +""" +struct s { int v; }; +s !ptr=new s; +while( -arg ) + ptr = new s; +while(1) + arg = arg+ptr.v; +"""); + code.parse().opto(); + assertEquals("return Top;", code.print()); + } + + @Test + public void testAntiDeps7() { + CodeGen code = new CodeGen( +""" +struct S { int f; }; +S !v = new S; +S t = new S; +int i = v.f; +while (arg) { + v.f = arg; + arg = arg-1; +} +return i; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testAntiDeps8() { + CodeGen code = new CodeGen( +""" +struct S { int f; }; +S !v = new S; +S t = new S; +while(arg) { + arg=arg-1; + int f = v.f; + v.f = 2; + if (arg) arg = f; + else v.f = 3; +} +return arg; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testAntiDeps9() { + CodeGen code = new CodeGen( +""" +struct S { int f; }; +S !v = new S; +S t = new S; +if (arg) { + v.f=2; + int i=t.f; + v.f=i; +} +return v; +"""); + code.parse().opto(); + assertEquals("return S;", code.print()); + } + + @Test + public void testExample2() { + CodeGen code = new CodeGen( +""" +struct S { int f; }; +S !v = new S; +int i = arg; +while (arg > 0) { + int j = i/3; + if (arg == 5) + v.f = j; + arg = arg - 1; +} +return v; + """); + code.parse().opto(); + //assertEquals("return new S;", code.print()); + } + + @Test + public void testScheduleUse() { + CodeGen code = new CodeGen( +""" +int v0=0; +while(0>=0) { + u1 v1=0; + v1=v0; + if(v1*0) + v0=-v1; +} +return 0; +"""); + code.parse().opto(); + assertEquals("return Top;", code.print()); + } + + @Test + public void testLoopCarriedDep() { + CodeGen code = new CodeGen( +""" +u32 v0=0; +{ + int v1=0; + while(v1) { + v1=1>>>v0!=0; + v0=v1/0; + } + return v1; +} +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter12Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter12Test.java new file mode 100644 index 00000000..21df8205 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter12Test.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.evaluator.Evaluator; +import com.seaofnodes.simple.node.StopNode; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter12Test { + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 3.14; +"""); + code.parse().opto(); + assertEquals("return 3.14;", code.print()); + assertEquals("3.14", Eval2.eval(code, 0)); + } + + @Test + public void testFloat() { + CodeGen code = new CodeGen( +""" +return 3.14; +"""); + code.parse().opto(); + assertEquals("return 3.14;", code.print()); + assertEquals("3.14", Eval2.eval(code, 0)); + } + + @Test + public void testSquareRoot() { + CodeGen code = new CodeGen( +""" +flt guess = arg; +while( 1 ) { + flt next = (arg/guess + guess)/2; + if( next == guess ) break; + guess = next; +} +return guess; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,(flt)arg,(((ToFloat/Phi_guess)+Phi_guess)/2.0f));", code.print()); + assertEquals("3.0", Eval2.eval(code, 9)); + assertEquals("1.414213562373095", Eval2.eval(code, 2)); + } + + @Test + public void testFPOps() { + CodeGen code = new CodeGen( +""" +flt x = arg; +return x+1==x; +"""); + code.parse().opto(); + assertEquals("return ((flt)arg==(ToFloat+1.0f));", code.print()); + assertEquals("0", Eval2.eval(code, 1)); + } +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter13Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter13Test.java new file mode 100644 index 00000000..d05fd7d8 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter13Test.java @@ -0,0 +1,247 @@ +package com.seaofnodes.simple; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter13Test { + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 3.14; +"""); + code.parse().opto(); + assertEquals("return 3.14;", code.print()); + assertEquals("3.14", Eval2.eval(code, 0)); + } + + @Test + public void testLinkedList0() { + CodeGen code = new CodeGen( +""" +struct LLI { LLI? next; int i; }; +LLI? !head = null; +while( arg ) { + LLI !x = new LLI; + x.next = head; + x.i = arg; + head = x; + arg = arg-1; +} +return head.next.i; +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Might be null accessing 'i'",e.getMessage()); } + } + + @Test + public void testLinkedList1() { + CodeGen code = new CodeGen( +""" +struct LLI { LLI? next; int i; }; +LLI? !head = null; +while( arg ) { + LLI !x = new LLI; + x.next = head; + x.i = arg; + head = x; + arg = arg-1; +} +if( !head ) return 0; +LLI? next = head.next; +if( next==null ) return 1; +return next.i; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,0,1,.i);", code.print()); + assertEquals("2", Eval2.eval(code, 3)); + } + + @Test + public void testCoRecur() { + CodeGen code = new CodeGen( +""" +struct int0 { int i; flt0? f; }; +struct flt0 { flt f; int0? i; }; +int0 !i0 = new int0; +i0.i = 17; +flt0 !f0 = new flt0; +f0.f = 3.14; +i0.f = f0; +f0.i = i0; +return f0.i.f.i.i; +"""); + code.parse().opto(); + assertEquals("return 17;", code.print()); + } + + @Test + public void testNullRef0() { + CodeGen code = new CodeGen( +""" +struct N { N? next; int i; }; +N n = new N; +return n.next; +"""); + code.parse().opto(); + assertEquals("return null;", code.print()); + } + + @Test + public void testNullRef1() { + CodeGen code = new CodeGen( +""" +struct M { int m; }; +struct N { M next; int i; }; +N n = new N { next = new M; }; +return n.next; +"""); + code.parse().opto(); + assertEquals("return (const)M;", code.print()); + } + + @Test + public void testNullRef2() { + CodeGen code = new CodeGen( +""" +struct M { int m; }; +struct N { M next; int i; }; +N n = new N { next = null; } +return n.next; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Type null is not of declared type *M",e.getMessage()); } + } + + @Test + public void testNullRef3() { + CodeGen code = new CodeGen( +""" +struct N { N? next; int i; }; +N !n = new N; +n.i = 3.14; +return n.i; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Type 3.14 is not of declared type int",e.getMessage()); } + } + + @Test + public void testEmpty() { + CodeGen code = new CodeGen( +""" +struct S{}; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + + @Test + public void testForwardRef0() { + CodeGen code = new CodeGen( +""" +struct S1 { S2? s; }; +return new S2; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Unknown struct type 'S2'",e.getMessage()); } + } + + @Test + public void testForwardRef1() { + CodeGen code = new CodeGen( +""" +struct S1 { S2? s; }; +struct S2 { int x; }; +return new S1.s=new S2; +"""); + code.parse().opto(); + assertEquals("return S2;", code.print()); + } + + @Test + public void testcheckNull() { + CodeGen code = new CodeGen( +""" +struct I {int i;}; +struct P { I? pi; }; +P !p1 = new P; +P !p2 = new P; +p2.pi = new I; +p2.pi.i = 2; +if (arg) p1 = new P; +return p1.pi.i + 1; +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Might be null accessing 'i'",e.getMessage()); } + } + + @Test + public void testCoRecur2() { + CodeGen code = new CodeGen( +""" +struct A { L? a; T? b; F? c; }; A? !a = new A; +struct B { M? a; U? b; G? c; }; B? !b = new B; +struct C { N? a; V? b; H? c; }; C? !c = new C; +struct D { O? a; W? b; I? c; }; D? !d = new D; +struct E { P? a; X? b; J? c; }; E? !e = new E; +struct F { Q? a; Y? b; K? c; }; F? !f = new F; +struct G { R? a; Z? b; L? c; }; G? !g = new G; +struct H { S? a; A? b; M? c; }; H? !h = new H; +struct I { T? a; B? b; N? c; }; I? !i = new I; +struct J { U? a; C? b; O? c; }; J? !j = new J; +struct K { V? a; D? b; P? c; }; K? !k = new K; +struct L { W? a; E? b; Q? c; }; L? !l = new L; +struct M { X? a; F? b; R? c; }; M? !m = new M; +struct N { Y? a; G? b; S? c; }; N? !n = new N; +struct O { Z? a; H? b; T? c; }; O? !o = new O; +struct P { A? a; I? b; U? c; }; P? !p = new P; +struct Q { B? a; J? b; V? c; }; Q? !q = new Q; +struct R { C? a; K? b; W? c; }; R? !r = new R; +struct S { D? a; L? b; X? c; }; S? !s = new S; +struct T { E? a; M? b; Y? c; }; T? !t = new T; +struct U { F? a; N? b; Z? c; }; U? !u = new U; +struct V { G? a; O? b; A? c; }; V? !v = new V; +struct W { H? a; P? b; B? c; }; W? !w = new W; +struct X { I? a; Q? b; C? c; }; X? !x = new X; +struct Y { J? a; R? b; D? c; }; Y? !y = new Y; +struct Z { K? a; S? b; E? c; }; Z? !z = new Z; + +a.a=l; a.b=t; a.c=f; +b.a=m; b.b=u; b.c=g; +c.a=n; c.b=v; c.c=h; +d.a=o; d.b=w; d.c=i; +e.a=p; e.b=x; e.c=j; +f.a=q; f.b=y; f.c=k; +g.a=r; g.b=z; g.c=l; +h.a=s; h.b=a; h.c=m; +i.a=t; i.b=b; i.c=n; +j.a=u; j.b=c; j.c=o; +k.a=v; k.b=d; k.c=p; +l.a=w; l.b=e; l.c=q; +m.a=x; m.b=f; m.c=r; +n.a=y; n.b=g; n.c=s; +o.a=z; o.b=h; o.c=t; +p.a=a; p.b=i; p.c=u; +q.a=b; q.b=j; q.c=v; +r.a=c; r.b=k; r.c=w; +s.a=d; s.b=l; s.c=x; +t.a=e; t.b=m; t.c=y; +u.a=f; u.b=n; u.c=z; +v.a=g; v.b=o; v.c=a; +w.a=h; w.b=p; w.c=b; +x.a=i; x.b=q; x.c=c; +y.a=j; y.b=r; y.c=d; +z.a=k; z.b=s; z.c=e; + +return a.b.c.a.b.c.a.b.c.a.b.c.a.b.c.a.b.c; + +"""); + code.parse().opto().typeCheck().GCM().localSched(); + assertEquals("return R;", code._stop.toString()); + assertEquals("R{a=C{a=N{a=Y{a=J{a=U{a=F{a=Q{a=B{a=M{a=X{a=I{a=T{a=E{a=P{a=A{a=L{a=W{a=H{a=S{a=D{a=O{a=Z{a=K{a=V{a=G{a=$cyclic,b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic}", Eval2.eval(code, 0)); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter14Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter14Test.java new file mode 100644 index 00000000..17708c37 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter14Test.java @@ -0,0 +1,226 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.StopNode; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter14Test { + + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 3.14; +"""); + code.parse().opto(); + assertEquals("return 3.14;", code.print()); + assertEquals("3.14", Eval2.eval(code, 0)); + } + + @Test + public void testRange() { + CodeGen code = new CodeGen( +""" +int b; +if( arg ) b=1; else b=0; +int c = 99; +if( b < 0 ) c = -1; +if( b > 2 ) c = 1; +return c; +"""); + code.parse().opto(); + assertEquals("return 99;", code.print()); + assertEquals("99", Eval2.eval(code, 0)); + } + + @Test + public void testU8() { + CodeGen code = new CodeGen( +""" +u8 b = 123; +b = b + 456;// Truncate +return b; +"""); + code.parse().opto(); + assertEquals("return 67;", code.print()); + assertEquals("67", Eval2.eval(code, 0)); + } + + + @Test + public void testU8While() { + CodeGen code = new CodeGen( +""" +u8 b = 123; +while( b ) b = b + 456;// Truncate +return b; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testU1() { + CodeGen code = new CodeGen( +""" +bool b = 123; +b = b + 456;// Truncate +u1 c = b; // No more truncate needed +return c; +"""); + code.parse().opto(); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test + public void testAnd() { + CodeGen code = new CodeGen( +""" +int b = 123; +b = b+456 & 31; // Precedence +return b; +"""); + code.parse().opto(); + assertEquals("return 3;", code.print()); + assertEquals("3", Eval2.eval(code, 0)); + } + + @Test + public void testRefLoad() { + CodeGen code = new CodeGen( +""" +struct Foo { u1 b; }; +Foo !f = new Foo; +f.b = 123; +return f.b; +"""); + code.parse().opto(); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test + public void testSigned() { + CodeGen code = new CodeGen( +""" +i8 b = 255; // Chopped +return b; // Sign extend +"""); + code.parse().opto(); + assertEquals("return -1;", code.print()); + assertEquals("-1", Eval2.eval(code, 0)); + } + + @Test + public void testI8() { + CodeGen code = new CodeGen( +""" +i8 b = arg; +b = b + 1;// Truncate +return b; +"""); + code.parse().opto(); + assertEquals("return (((((arg<<56)>>56)+1)<<56)>>56);", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + assertEquals("-128", Eval2.eval(code, 127)); + } + + @Test + public void testMask() { + CodeGen code = new CodeGen( +""" +u16 mask = (1<<16)-1; // AND mask +int c = 123456789 & mask; +return c; // +"""); + code.parse().opto(); + assertEquals("return 52501;", code.print()); + assertEquals("52501", Eval2.eval(code, 0)); + } + + @Test + public void testOr() { + CodeGen code = new CodeGen( +""" +return (arg | 123 ^ 456) >>> 1; +"""); + code.parse().opto(); + assertEquals("return (((arg|123)^456)>>>1);", code.print()); + assertEquals("217", Eval2.eval(code, 0)); + } + + @Test + public void testMaskFloat() { + CodeGen code = new CodeGen( +""" +flt f = arg; +arg = f & 1; +return arg; +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Cannot '&' flt",e.getMessage()); } + } + + @Test + public void testCloneAnd() { + CodeGen code = new CodeGen(""" +int v0=0; +u32 v1 = 1&(1<<arg)&(1<<arg); +while(arg) v1=-v0; +while(v1) break; +return v1; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,(((1<<arg)&1)&Shl),0);", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test + public void testAndHigh() { + CodeGen code = new CodeGen(""" +int v0=0; +if(0&0>>>0) { + while(0) { + u8 v1=0; + v0=0>>>0; + v1=arg; + while(v1+0) {} + } +} +return v0; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + + @Test + public void testTypes() { + CodeGen code = new CodeGen( +""" +i8 xi8 = 123456789; if( xi8 != 21 ) return -8; +i16 xi16 = 123456789; if( xi16 != -13035 ) return -16; +i32 xi32 = 123456789; if( xi32 != 123456789 ) return -32; +i64 xi64 = 123456789; if( xi64 != 123456789 ) return -64; +int xint = 123456789; if( xint != 123456789 ) return -64; + +u1 ui1 = 123456789; if( ui1 != 1 ) return 1; +u8 ui8 = 123456789; if( ui8 != 21 ) return 8; +u16 ui16 = 123456789; if( ui16 != 52501 ) return 16; +u32 ui32 = 123456789; if( ui32 != 123456789 ) return 32; + +flt fflt = 3.141592653589793; if( fflt != 3.141592653589793 ) return 3; +f64 ff64 = 3.141592653589793; if( ff64 != 3.141592653589793 ) return 3; +f32 ff32 = 3.141592653589793; if( ff32 != 3.1415927410125732) return 5; + +return 0; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter15Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter15Test.java new file mode 100644 index 00000000..546eb0e9 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter15Test.java @@ -0,0 +1,380 @@ +package com.seaofnodes.simple; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class Chapter15Test { + + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 3.14; +"""); + code.parse().opto(); + assertEquals("return 3.14;", code.print()); + assertEquals("3.14", Eval2.eval(code, 0)); + } + + @Test + public void testCyclic() { + CodeGen code = new CodeGen( +""" +struct C { C? l; }; +C !c = new C; +c.l = c; +return c; +"""); + code.parse().opto(); + assertEquals("return C;", code.print()); + assertEquals("C{l=$cyclic}", Eval2.eval(code, 0)); + } + + @Test + public void testSafetyCheck() { + CodeGen code = new CodeGen( +""" +u8[] old = new u8[0]; +u8[] !output = new u8[1]; +int i = 0; +while (i < old#) { + output[i] = old[i]; + i = i + 1; +} +output[i] = 1; +return output; +"""); + code.parse().opto(); + assertEquals("return [u8];", code.print()); + assertEquals("\u0001", Eval2.eval(code, 0)); + } + + @Test + public void testBasic1() { + CodeGen code = new CodeGen( +""" +int[] is = new int[2]; +return is[1]; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + + @Test + public void testBasic2() { + CodeGen code = new CodeGen( +""" +int[] is = new int[2]; +int[] is2 = new int[2]; +return is[1]; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + + @Test + public void testBasic3() { + CodeGen code = new CodeGen( +""" +int[] !a = new int[2]; +a[0] = 1; +a[1] = 2; +return a[0]; +"""); + code.parse().opto(); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test + public void testBasic4() { + CodeGen code = new CodeGen( +""" +struct A { int i; }; +A?[] !a = new A?[2]; +return a; +"""); + code.parse().opto(); + assertEquals("return [*A?];", code.print()); + assertEquals("*A {int !i; }?[ null,null]", Eval2.eval(code, 0)); + } + + @Test + public void testBasic5() { + CodeGen code = new CodeGen( +""" +struct S { int x; flt y; }; +// A new S +S !s = new S; s.x=99; s.y = 3.14; + +// Double-d array of Ss. Fill in one row. +S?[]?[] !iss = new S?[]?[2]; +iss[0] = new S?[7]; +iss[0][2] = s; + +// Now pull out the filled-in value, with null checks +flt rez; +S?[]? is = iss[arg]; +if( !is ) rez = 1.2; +else { + S? i = is[2]; + if( !i ) rez = 2.3; + else rez = i.y; +} +return rez; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,1.2,Phi(Region,2.3,.y));", code.print()); + assertEquals("3.14", Eval2.eval(code, 0)); + assertEquals("1.2", Eval2.eval(code, 1)); + } + + @Test + public void testBasic6() { + CodeGen code = new CodeGen( +""" +struct S { int x; flt y; }; +// A new S +S !s = new S; s.x=99; s.y = 3.14; + +// Double-d array of Ss. Fill in one row. +S?[]?[] !iss = new S?[]?[2]; +iss[0] = new S?[7]; +iss[0][2] = s; + +// Now pull out the filled-in value, with null checks +flt rez = 1.2; +if( iss[arg] ) + if( iss[arg][2] ) + rez = iss[arg][2].y; +return rez;"""); + code.parse().opto().typeCheck(); + assertEquals("return Phi(Region,Phi(Region,.y,1.2),1.2);", code.print()); + assertEquals("3.14", Eval2.eval(code, 0)); + try { Eval2.eval(code, 10); fail(); } // Fails AIOOBE in Eval2, no range checks + catch( Exception e ) { assertEquals("Index 10 out of bounds for length 2",e.getMessage()); } + } + + @Test + public void testTree() { + CodeGen code = new CodeGen( +""" +// Can we define a forward-reference array? +struct Tree { Tree?[]? _kids; }; +Tree !root = new Tree; +root._kids = new Tree?[2]; // NO BANG SO ARRAY IS OF IMMUTABLE TREES???? +root._kids[0] = new Tree; +return root; +"""); + code.parse().opto(); + assertEquals("return Tree;", code.print()); + assertEquals("Tree{_kids=*Tree?[ Tree{_kids=null},null]}", Eval2.eval(code, 0)); + } + + @Test + public void testNestedStructAddMemProj() { + CodeGen code = new CodeGen( +""" +struct S { int a; int[] b; }; +return 0; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testRollingSum() { + CodeGen code = new CodeGen( +""" +int[] !ary = new int[arg]; +// Fill [0,1,2,3,4,...] +int i=0; +while( i < ary# ) { + ary[i] = i; + i = i+1; +} +// Fill [0,1,3,6,10,...] +i=0; +while( i < ary# - 1 ) { + ary[i+1] = ary[i+1] + ary[i]; + i = i+1; +} +return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6 +"""); + code.parse().opto(); + assertEquals("return (.[]+(.[]*1000));", code.print()); + assertEquals("1006", Eval2.eval(code, 4)); + } + + @Test + public void sieveOEratosthenes() { + CodeGen code = new CodeGen( +""" +int[] !ary = new int[arg], !primes = new int[arg]; +int nprimes = 0, p=2; +// Find primes while p^2 < arg +while( p*p < arg ) { + // skip marked non-primes + while( ary[p]==1 ) p = p + 1; + // p is now a prime + primes[nprimes] = p; nprimes = nprimes + 1; + // Mark out the rest non-primes + int i = p + p; + while( i < ary# ) { + ary[i] = 1; + i = i + p; + } + p = p + 1; +} +// Now just collect the remaining primes, no more marking +while( p < arg ) { + if( ary[p] == 0 ) { + primes[nprimes] = p; nprimes = nprimes + 1; + } + p = p + 1; +} +// Copy/shrink the result array +int[] !rez = new int[nprimes]; +int j = 0; +while( j < nprimes ) { + rez[j] = primes[j]; + j = j + 1; +} +return rez; +"""); + code.parse().opto(); + assertEquals("return [int];", code.print()); + assertEquals("int[ 2,3,5,7,11,13,17,19]",Eval2.eval(code, 20)); + } + + @Test + public void testNewNodeInit() { + CodeGen code = new CodeGen( +""" +struct S {int i; flt f;}; +S !s1 = new S; +S !s2 = new S; +s2.i = 3; +s2.f = 2.0; +if (arg) s1 = new S; +return s1.i + s1.f; +"""); + code.parse().opto(); + assertEquals("return ((flt).i+.f);", code.print()); + assertEquals("0.0", Eval2.eval(code, 0)); + } + + + @Test + public void testBad0() { + CodeGen code = new CodeGen( +""" +return new flt; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Cannot allocate a flt",e.getMessage()); } + } + + @Test + public void testBad1() { + CodeGen code = new CodeGen( +""" +int is = new int[2]; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Type *[int] is not of declared type int",e.getMessage()); } + } + + @Test + public void testBad2() { + CodeGen code = new CodeGen( +""" +int[] is = new int[3.14]; +return is[1]; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Cannot allocate an array with length 3.14",e.getMessage()); } + } + + @Test + public void testBad3() { + CodeGen code = new CodeGen( +""" +int[] is = new int[arg]; +return is[1]; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 4)); + try { Eval2.eval(code,0); } + catch( ArrayIndexOutOfBoundsException e ) { assertEquals("Array index 1 out of bounds for array length 0",e.getMessage()); } + try { Eval2.eval(code,-1); } + catch( NegativeArraySizeException e ) { assertEquals("-1",e.getMessage()); } + } + + @Test + public void testProgress() { + CodeGen code = new CodeGen(""" +i8 v1=0&0; +u8 v2=0; +byte v4=0; +if(0) {} +while(v2<0) { + v4=0-v1; + break; +} +int v5=0&0; +while(v5+(0&0)) { + int v7=0&0; + while(v7) + v4=0>>>v5; + while(v1) + return 0; +} +return v1; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + + @Test + public void testSharpNot() { + CodeGen code = new CodeGen( +""" +if(0>>0) {} +while(0) {} +u32 v7=0; +int v8=0; +while(0<--1>>>---(v7*0==v8)) {} +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } + + + @Test + public void testProgress2() { + CodeGen code = new CodeGen( +""" +if(1) {} +else { + while(arg>>>0&0>>>0) {} + byte v3=0>>>0; + while(0) {} + int v7=0>>>0; + while(v7<0>>>0) { + while(0+v7<=0) if(1) arg=-12; + if(arg) { + v3=arg+v3+0; + arg=0; + } + } +} +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + } +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter16Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter16Test.java new file mode 100644 index 00000000..ad3dc690 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter16Test.java @@ -0,0 +1,260 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.evaluator.Evaluator; +import com.seaofnodes.simple.node.StopNode; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter16Test { + + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 3.14; +"""); + code.parse().opto(); + assertEquals("return 3.14;", code.print()); + assertEquals("3.14", Eval2.eval(code, 0)); + } + + + @Test + public void testMulti0() { + CodeGen code = new CodeGen( +""" +int x, y; +return x+y; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + @Test + public void testMulti1() { + CodeGen code = new CodeGen( +""" +int x=2, y=x+1; +return x+y; +"""); + code.parse().opto(); + assertEquals("return 5;", code.print()); + assertEquals("5", Eval2.eval(code, 0)); + } + +// @Test +// public void testFinal0() { +// CodeGen code = new CodeGen( +//""" +//int !x=2; +//x=3; +//return x; +//"""); +// try { code.parse().opto(); fail(); } +// catch( Exception e ) { assertEquals("Cannot reassign final 'x'",e.getMessage()); } +// } + + @Test + public void testFinal1() { + CodeGen code = new CodeGen( +""" +int x=2, y=3; +if( arg ) { int x = y; x = x*x; y=x; } // Shadow final x +return y; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,9,3);", code.print()); + assertEquals("3", Eval2.eval(code, 0)); + assertEquals("9", Eval2.eval(code, 1)); + } + + @Test + public void testConstruct0() { + CodeGen code = new CodeGen(""" +struct X { int x=3; }; +X z = new X; +return z.x; +"""); + code.parse().opto(); + assertEquals("return 3;", code.print()); + assertEquals("3", Eval2.eval(code, 0)); + } + + @Test + public void testConstruct1() { + CodeGen code = new CodeGen(""" +struct X { int !x; }; +X z = new X { x=3; }; +return z.x; +"""); + code.parse().opto(); + assertEquals("return 3;", code.print()); + assertEquals("3", Eval2.eval(code, 0)); + } + + @Test + public void testConstruct2() { + CodeGen code = new CodeGen(""" +struct X { int x=3; }; +X z = new X { x = 4; }; +return z.x; +"""); + code.parse().opto(); + assertEquals("return 4;", code.print()); + assertEquals("4", Eval2.eval(code, 0)); + } + + + @Test + public void testStructFinal0() { + CodeGen code = new CodeGen(""" +struct Point { int !x, !y; }; +Point p = new Point { x=3; y=4; }; +return p; +"""); + code.parse().opto(); + assertEquals("return (const)Point;", code.print()); + assertEquals("Point{x=3,y=4}", Eval2.eval(code, 0)); + } + + @Test + public void testStructFinal1() { + CodeGen code = new CodeGen(""" +struct Point { int x=3, y=4; }; +Point p = new Point { x=5; y=6; }; +p.x++; +return p; +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); } + } + + @Test + public void testStructFinal2() { + CodeGen code = new CodeGen(""" +struct Point { int x=3, y=4; }; +Point p = new Point; +p.x++; +return p; +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); } + } + + @Test + public void testStructFinal3() { + CodeGen code = new CodeGen(""" +struct Point { var x; var y; }; +Point p = new Point; +p.x++; +return p; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("'Point' is not fully initialized, field 'x' needs to be set in a constructor",e.getMessage()); } + } + + @Test + public void testStructFinal4() { + CodeGen code = new CodeGen(""" +struct Point { val x=3; val y=4; }; +Point p = new Point; +p.x++; +return p; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Cannot reassign final 'x'",e.getMessage()); } + } + + @Test + public void testStructFinal5() { + CodeGen code = new CodeGen(""" +struct Point { var x=3; var y=4; }; +Point !p = new Point; +p.x++; +return p; +"""); + code.parse().opto(); + assertEquals("return Point;", code.print()); + assertEquals("Point{x=4,y=4}", Eval2.eval(code, 0)); + } + + // Same as the Chapter13 test with the same name, but using the new + // constructor syntax + @Test + public void testLinkedList1() { + CodeGen code = new CodeGen( +""" +struct LLI { LLI? next; int i; }; +LLI? !head = null; +while( arg ) { + head = new LLI { next=head; i=arg; }; + arg = arg-1; +} +if( !head ) return 0; +LLI? next = head.next; +if( !next ) return 1; +return next.i; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,0,1,.i);", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + assertEquals("1", Eval2.eval(code, 1)); + assertEquals("2", Eval2.eval(code, 3)); + } + + @Test + public void testLinkedList2() { + CodeGen code = new CodeGen( +""" +struct LLI { LLI? next; int i; }; +LLI? !head = null; +while( arg ) { + head = new LLI { + next=head; + // Any old code in the constructor + int !tmp=arg; + while( arg > 10 ) { + tmp = tmp + arg; + arg = arg - 1; + } + i=tmp; + }; + arg = arg-1; +} +if( !head ) return 0; +LLI? next = head.next; +if( !next ) return 1; +return next.i; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,0,1,.i);", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + assertEquals("1", Eval2.eval(code, 1)); + assertEquals("2", Eval2.eval(code, 11)); + } + + @Test + public void testSquare() { + CodeGen code = new CodeGen( +""" +struct Square { + flt !side = arg; + // Newtons approximation to the square root, computed in a constructor. + // The actual allocation will copy in this result as the initial + // value for 'diag'. + flt !diag = arg*arg/2; + while( 1 ) { + flt next = (side/diag + diag)/2; + if( next == diag ) break; + diag = next; + } +}; +return new Square; +"""); + code.parse().opto(); + assertEquals("return Square;", code.print()); + assertEquals("Square{side=3.0,diag=1.7320508075688772}", Eval2.eval(code, 3)); + assertEquals("Square{side=4.0,diag=2.0}", Eval2.eval(code, 4)); + } +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter17Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter17Test.java new file mode 100644 index 00000000..a1d0f8b9 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter17Test.java @@ -0,0 +1,556 @@ +package com.seaofnodes.simple; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter17Test { + + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +int i = 0; +i=i=1; +return i; +"""); + code.parse().opto(); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + + // --------------------------------------------------------------- + @Test + public void testInc0() { + CodeGen code = new CodeGen(""" +return arg++; +"""); + code.parse().opto(); + assertEquals("return arg;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + + @Test + public void testInc1() { + CodeGen code = new CodeGen(""" +return arg+++arg++; +"""); + code.parse().opto(); + assertEquals("return ((arg<<1)+1);", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test + public void testInc2() { + CodeGen code = new CodeGen(""" +// -(arg--)-(arg--) +return -arg---arg--; +"""); + code.parse().opto(); + assertEquals("return (-((arg<<1)+-1));", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test + public void testInc3() { + CodeGen code = new CodeGen(""" +int[] !xs = new int[arg]; +xs[0]++; +xs[1]++; +return xs[0]; +"""); + code.parse().opto(); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 2)); + } + + @Test + public void testInc4() { + CodeGen code = new CodeGen(""" +u8[] !xs = new u8[1]; +xs[0]--; +return xs[0]; +"""); + code.parse().opto(); + assertEquals("return 255;", code.print()); + assertEquals("255", Eval2.eval(code, 2)); + } + + @Test + public void testInc5() { + CodeGen code = new CodeGen(""" +struct S { u16 x; }; +S !s = new S; +s.x--; +return s.x; +"""); + code.parse().opto(); + assertEquals("return 65535;", code.print()); + assertEquals("65535", Eval2.eval(code, 2)); + } + + @Test + public void testInc6() { + CodeGen code = new CodeGen("return --arg;"); + code.parse().opto(); + assertEquals("return (arg+-1);", code.print()); + assertEquals("-1", Eval2.eval(code, 0)); + } + + @Test + public void testInc7() { + CodeGen code = new CodeGen("u8 x=0; return --x;"); + code.parse().opto(); + assertEquals("return 255;", code.print()); + assertEquals("255", Eval2.eval(code, 0)); + } + + @Test + public void testInc8() { + CodeGen code = new CodeGen("int x; x+=2; return x+=3;"); + code.parse().opto(); + assertEquals("return 5;", code.print()); + assertEquals("5", Eval2.eval(code, 0)); + } + + @Test + public void testInc9() { + CodeGen code = new CodeGen("flt x = 1.5; x++; return x;"); + code.parse().opto(); + assertEquals("return 2.5f;", code.print()); + assertEquals("2.5", Eval2.eval(code, 0)); + } + + @Test public void testInt10() { + CodeGen code = new CodeGen("flt x = 1.5; x++; int i = x; return i;"); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Type 2.5f is not of declared type int",e.getMessage()); } + } + + + // --------------------------------------------------------------- + @Test public void testVar0() { + CodeGen code = new CodeGen("var d; return d;"); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Syntax error, expected =expression: ;",e.getMessage()); } + } + + @Test public void testVar1() { + CodeGen code = new CodeGen("val d; return d;"); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Syntax error, expected =expression: ;",e.getMessage()); } + } + + @Test public void testVar2() { + CodeGen code = new CodeGen("int x; x=3; x++; return x; // Ok, no initializer so x is mutable "); + code.parse().opto(); + assertEquals("return 4;", code.print()); + assertEquals("4", Eval2.eval(code, 0)); + } + + @Test public void testVar3() { + CodeGen code = new CodeGen("int x=3; x++; return x; // Ok, primitive so x is mutable despite initializer "); + code.parse().opto(); + assertEquals("return 4;", code.print()); + assertEquals("4", Eval2.eval(code, 0)); + } + + @Test public void testVar4() { + CodeGen code = new CodeGen("struct S{int x;}; S? s; s=new S; s.x++; return s.x; // Ok, no initializer so x is mutable "); + code.parse().opto(); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test public void testVar5() { + CodeGen code = new CodeGen("struct S{int x;}; S? s; s=new S{x=3;}; s.x++; return s.x; // Ok, no initializer so x is mutable "); + code.parse().opto(); + assertEquals("return 4;", code.print()); + assertEquals("4", Eval2.eval(code, 0)); + } + + @Test public void testVar6() { + CodeGen code = new CodeGen("struct S{int x;}; S s=new S; s.x++; return s.x; // Error initializer so x is immutable "); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); } + } + + @Test public void testVar7() { + CodeGen code = new CodeGen("struct S{int x;}; S s=new S{x=3;}; s.x++; return s.x; // Error initializer so x is immutable "); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); } + } + + @Test public void testVar8() { + CodeGen code = new CodeGen("struct S{int x;}; S !s=new S; s.x++; return s.x; // Ok, has '!' so s.x is mutable "); + code.parse().opto(); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test public void testVar9() { + CodeGen code = new CodeGen("struct S{int x;}; var s=new S; s.x++; return s.x; // Ok, has var so s.x is mutable "); + code.parse().opto(); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test public void testVar10() { + CodeGen code = new CodeGen("struct S{int x;}; val s=new S; s.x++; return s.x; // Error, has val so x is immutable "); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); } + } + + @Test public void testVar11() { + CodeGen code = new CodeGen(""" +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 + +val xfoo = foo; // Throw away mutability +xfoo.bar.x++; // Error, cannot mutate through xfoo + """); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); } + } + + @Test public void testVar12() { + CodeGen code = new CodeGen(""" +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 + +val 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; +"""); + code.parse().opto(); + assertEquals("return 45;", code.print()); + assertEquals("45", Eval2.eval(code, 0)); + } + + @Test + public void testVar13() { + CodeGen code = new CodeGen(""" +int i,i++; +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Redefining name 'i'",e.getMessage()); } + } + + + @Test + public void testVar14() { + CodeGen code = new CodeGen(""" +struct B {}; +struct A { B b; }; +A x = new A { + return b; // read before init + b = new B; +}; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Cannot read uninitialized field 'b'",e.getMessage()); } + } + + @Test + public void testVar15() { + CodeGen code = new CodeGen(""" +struct B {}; +struct A { B b; }; +return new A { + if (arg) b = new B; // Constructor ends with partial init of b +}.b; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("'A' is not fully initialized, field 'b' needs to be set in a constructor",e.getMessage()); } + } + + @Test + public void testVar16() { + CodeGen code = new CodeGen( +""" +struct S{}; +val x = 1; +val s = new S{x = 2;}; +return x; +""" +); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Cannot reassign final 'x'",e.getMessage()); } + } + + @Test + public void testVar17() { + CodeGen code = new CodeGen( +""" +struct Person { + bool focused; +}; +val focus = { Person p -> + p.focused = true; +}; +Person me; // Person !me = new Person; +focus(me); +return me; +""" +); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Syntax error, expected =expression: ;",e.getMessage()); } + } + + // --------------------------------------------------------------- + @Test + public void testTrinary0() { + CodeGen code = new CodeGen(""" +return arg ? 1 : 2; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,1,2);", code.print()); + assertEquals("2", Eval2.eval(code, 0)); + assertEquals("1", Eval2.eval(code, 1)); + } + + @Test + public void testTrinary1() { + CodeGen code = new CodeGen(""" +return arg ? 0 : arg; +"""); + code.parse().opto(); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + + @Test + public void testTrinary2() { + CodeGen code = new CodeGen(""" +struct Bar { int x; }; +var b = arg ? new Bar : null; +return b ? b.x++ + b.x++ : -1; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,((.x<<1)+1),-1);", code.print()); + assertEquals("-1", Eval2.eval(code, 0)); + assertEquals("1", Eval2.eval(code, 1)); + } + + @Test + public void testTrinary3() { + CodeGen code = new CodeGen(""" +struct Bar { int x; }; +var b = arg ? new Bar; +return b ? b.x++ + b.x++ : -1; +"""); + code.parse().opto(); + assertEquals("return Phi(Region,((.x<<1)+1),-1);", code.print()); + assertEquals("-1", Eval2.eval(code, 0)); + assertEquals("1", Eval2.eval(code, 1)); + } + + @Test + public void testTrinary4() { + // This test case will benefit from an unzipping transformation + CodeGen code = new CodeGen(""" +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" +"""); + code.parse().opto(); + assertEquals("return Phi(Region,Phi(Region,.x,.x),0);", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + assertEquals("1", Eval2.eval(code, 1)); + assertEquals("2", Eval2.eval(code, 2)); + } + + @Test + public void testTrinary5() { + CodeGen code = new CodeGen(""" +flt f=arg?1:1.2; +return f; // missing widening +"""); + code.parse().opto(); + assertEquals("return Phi(Region,1.0f,1.2);", code.print()); + assertEquals("1.2", Eval2.eval(code, 0)); + } + + + @Test + public void testTrinary6() { + CodeGen code = new CodeGen(""" +struct S{}; +return arg ? 7 : new S; +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("No common type amongst int and reference",e.getMessage()); } + } + + // --------------------------------------------------------------- + @Test + public void testFor0() { + CodeGen code = new CodeGen(""" +int sum=0; +for( int i=0; i<arg; i++ ) + sum += i; +return sum; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print()); + assertEquals("3", Eval2.eval(code, 3)); + assertEquals("45", Eval2.eval(code, 10)); + } + @Test + public void testFor1() { + CodeGen code = new CodeGen(""" +int sum=0, i=0; +for( ; i<arg; i++ ) + sum += i; +return sum; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print()); + assertEquals("3", Eval2.eval(code, 3)); + assertEquals("45", Eval2.eval(code, 10)); + } + @Test + public void testFor2() { + CodeGen code = new CodeGen(""" +int sum=0; +for( int i=0; ; i++ ) { + if( i>=arg ) break; + sum += i; +} +return sum; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print()); + assertEquals("3", Eval2.eval(code, 3)); + assertEquals("45", Eval2.eval(code, 10)); + } + @Test + public void testFor3() { + CodeGen code = new CodeGen(""" +int sum=0; +for( int i=0; i<arg; ) + sum += i++; +return sum; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print()); + assertEquals("3", Eval2.eval(code, 3)); + assertEquals("45", Eval2.eval(code, 10)); + } + @Test + public void testFor4() { + CodeGen code = new CodeGen(""" +int sum=0; +for( int i=0; i<arg; i++ ) + sum += i; +return i; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Undefined name 'i'",e.getMessage()); } + } + @Test + public void testFor5() { + CodeGen code = new CodeGen(""" +for(;;arg++;) {} +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Syntax error, expected Unexpected code after expression: ;",e.getMessage()); } + } + + + + // --------------------------------------------------------------- + @Test + public void testForward0() { + CodeGen code = new CodeGen(""" +struct A{ + B? f1; + B? f2; +}; +return new A; +"""); + code.parse().opto(); + assertEquals("return A;", code.print()); + assertEquals("A{f1=null,f2=null}", Eval2.eval(code,0)); + } + + @Test + public void testForward1() { + CodeGen code = new CodeGen(""" +struct A{ + B?[]? nil_array_of_b; + B?[] not_array_of_b = new B?[0]; +}; +return new A.not_array_of_b; +"""); + code.parse().opto(); + assertEquals("return (const)[*B?];", code.print()); + assertEquals("*B?[]", Eval2.eval(code,0)); + } + + // --------------------------------------------------------------- + @Test + public void testLinkedList2() { + CodeGen code = new CodeGen(""" +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; +"""); + code.parse().opto(); + assertEquals("return Phi(Loop,0,(Phi_sum+.i));", code.print()); + assertEquals("45", Eval2.eval(code, 10)); + } + + @Test + public void sieveOfEratosthenes() { + CodeGen code = new CodeGen( +""" +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; +"""); + code.parse().opto().typeCheck().GCM(); + assertEquals("return [int];", code.print()); + assertEquals("int[ 2,3,5,7,11,13,17,19]",Eval2.eval(code, 20)); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter18Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter18Test.java new file mode 100644 index 00000000..db970f60 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter18Test.java @@ -0,0 +1,369 @@ +package com.seaofnodes.simple; + +import org.junit.Test; +import static org.junit.Assert.*; +import static org.junit.Assert.fail; +import org.junit.Ignore; + +public class Chapter18Test { + + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 0; +"""); + code.parse().opto().typeCheck().GCM().localSched(); + assertEquals("return 0;", code._stop.toString()); + assertEquals("0", Eval2.eval(code, 2)); + } + + @Test + public void testPhiParalleAssign() { + CodeGen code = new CodeGen( +""" +int a = 1; +int b = 2; +while(arg--) { + int t = a; + a = b; + b = t; +} +return a; +"""); + code.parse().opto().typeCheck().GCM(); + assertEquals("return Phi(Loop,1,Phi(Loop,2,Phi_a));", code._stop.toString()); + assertEquals("1", Eval2.eval(code, 0)); + assertEquals("2", Eval2.eval(code, 1)); + assertEquals("1", Eval2.eval(code, 2)); + assertEquals("2", Eval2.eval(code, 3)); + } + + + // --------------------------------------------------------------- + @Test + public void testType0() { + CodeGen code = new CodeGen( +""" +{int -> int}? x2 = null; // null function ptr +return x2; +"""); + code.parse().opto(); + assertEquals("return null;", code._stop.toString()); + assertEquals("null", Eval2.eval(code, 0 ) ); + } + + @Test + public void testFcn0() { + CodeGen code = new CodeGen( +""" +{int -> int}? sq = { int x -> + x*x; +}; +return sq; +"""); + code.parse().opto(); + assertEquals("Stop[ return { sq}; return (Parm_x(sq,int)*x); ]", code._stop.toString()); + assertEquals("{ int -> int #1}", Eval2.eval(code, 3)); + } + + @Test + public void testFcn1() { + CodeGen code = new CodeGen( +""" +var sq = { int x -> + x*x; +}; +return sq(arg)+sq(3); +"""); + code.parse().opto().typeCheck().GCM().localSched(); + assertEquals("Stop[ return (sq( 3)+sq( arg)); return (Parm_x(sq,int,3,arg)*x); ]", code._stop.toString()); + assertEquals("13", Eval2.eval(code, 2)); + } + + // Function scope test + @Test + public void testFcn2() { + CodeGen code = new CodeGen( +""" +int cnt=1; +return { -> cnt; }; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("Variable 'cnt' is out of function scope and must be a final constant",e.getMessage()); } + } + + // Function scope test + @Test + public void testFcn3() { + CodeGen code = new CodeGen( +""" +val cnt=2; +return { -> cnt; }(); +"""); + code.parse().opto(); + assertEquals("return 2;", code._stop.toString()); + assertEquals("2", Eval2.eval(code, 0)); + } + + // Function variables + @Test + public void testFcn4() { + CodeGen code = new CodeGen( +""" +var fcn = arg ? { int x -> x*x; } : { int x -> x+x; }; +return fcn(3); +"""); + code.parse().opto(); + assertEquals("Stop[ return Phi(Region,{ int -> int #1},{ int -> int #2})( 3); return (Parm_x($fun,int,3)*x); return (Parm_x($fun,int,3)<<1); ]", code._stop.toString()); + assertEquals("6", Eval2.eval(code, 0)); + assertEquals("9", Eval2.eval(code, 1)); + } + + // Recursive factorial test + @Test + public void testFcn5() { + CodeGen code = new CodeGen("val fact = { int x -> x <= 1 ? 1 : x*fact(x-1); }; return fact(arg);"); + code.parse().opto().typeCheck(); + assertEquals("Stop[ return fact( arg); return Phi(Region,1,(Parm_x(fact,int,arg,(x-1))*fact( Sub))); ]", code._stop.toString()); + assertEquals( "1", Eval2.eval(code, 0)); + assertEquals( "1", Eval2.eval(code, 1)); + assertEquals( "2", Eval2.eval(code, 2)); + assertEquals( "6", Eval2.eval(code, 3)); + assertEquals("24", Eval2.eval(code, 4)); + } + + @Test + public void testFcn6() { + CodeGen code = new CodeGen( +""" +struct S { int i; }; +val newS = { int x -> return new S { i=x; }; }; +return newS(1).i; +"""); + code.parse().opto().typeCheck().GCM(); + assertEquals("return 1;", code._stop.toString()); + assertEquals("1", Eval2.eval(code, 0)); + } + + // Double forward reference + @Test + public void testFcn7() { + CodeGen code = new CodeGen( +""" +if( arg ? f : g ) return 1; +val f = {->1;}; +val g = {->2;}; +return 2; +"""); + code.parse().opto().typeCheck().GCM(); + assertEquals("Stop[ return 1; return 1; return 2; ]", code._stop.toString()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test + public void testFcn8() { + CodeGen code = new CodeGen( +""" +{int -> int}? i2i = null; +var id = {{int->int} f-> return f;}; +for(;;) { + if (i2i) return i2i(arg); + var x = {int i-> return i;}; + arg = x(3); + i2i = id(x); +} +"""); + code.parse().opto().typeCheck().GCM().localSched(); + assertEquals("Stop[ return x( Phi(Loop,arg,x( 3))); return Parm_i(x,int,3,Phi_arg); ]", code._stop.toString()); + assertEquals("3", Eval2.eval(code, 0)); + } + + @Test + public void testFcn9() { + CodeGen code = new CodeGen( +""" +{int -> int}? i2i = null; +for(;;) { + if (i2i) return i2i(arg); + var x = {int i-> return i;}; + arg = x(3); +} +"""); + code.parse().opto().typeCheck().GCM().localSched(); + assertEquals("return Top;", code._stop.toString()); + assertEquals(null, Eval2.eval(code, 0)); + } + + + @Test + public void testFcn10() { + CodeGen code = new CodeGen( +""" +struct Person { + int age; +}; + +val fcn = { Person?[] ps, int x -> + val tmp = ps[x]; + if( ps[x] ) + ps[x].age++; +}; + +var ps = new Person?[2]; +ps[0] = new Person; +ps[1] = new Person; +fcn(ps,1); +"""); + code.parse().opto().typeCheck().GCM().localSched(); + assertEquals("return 0;", code._stop.toString()); + assertEquals("0", Eval2.eval(code, 0)); + } + + // Function break + @Test + public void testErr1() { + CodeGen code = new CodeGen( +""" +for(;;) { + val f = { -> + break; + }; + f(); + return 2; +} +return 1; +"""); + try { code.parse().opto(); fail(); } + catch( Exception e ) { assertEquals("No active loop for a break or continue",e.getMessage()); } + } + + // Calling and inlining a null function + @Test + public void testErr2() { + CodeGen code = new CodeGen( +""" +{int -> int}? !i2i = { int i -> return i; }; +for(;;) { + if (i2i(2) == arg) break; + i2i = null; +} +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Might be null calling { int -> int #1}?",e.getMessage()); } + } + + + @Test + public void testErr3() { + CodeGen code = new CodeGen( +""" +val f = { int i, int j -> return i+j; }; +return f(); +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Expecting 2 arguments, but found 0",e.getMessage()); } + } + + + @Test + public void testErr4() { + CodeGen code = new CodeGen( +""" +struct S { + {int} f = { -> x(); return 0; }; // Do not let fref x be a field +}; +val x = { -> return 1; }; +S? s = null; +for(;;) { + if (s) return s.x; +} +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("Accessing unknown field 'x' from '*S'",e.getMessage()); } + } + + @Test + public void testErr5() { + CodeGen code = new CodeGen( +""" +val g = { -> + g(); +}; +return 0; +"""); + try { code.parse().opto().typeCheck(); fail(); } + catch( Exception e ) { assertEquals("No defined return type",e.getMessage()); } + } + + + // Mutual recursion. Fails without SCCP to lift the recursive return types. + @Ignore @Test + public void testFcnMutRec() { + CodeGen code = new CodeGen( +""" +val is_even = { int x -> x ? is_odd (x-1) : true ; }; +val is_odd = { int x -> x ? is_even(x-1) : false; }; +return is_even(arg); +"""); + code.parse().opto(); + assertEquals("Stop[ return is_even( arg); return Phi(Region,Phi(Region,is_even( ((Parm_x(is_even,int,arg,Sub)-1)-1)),0),1); ]", code._stop.toString()); + assertEquals("1", Eval2.eval(code, 0)); + assertEquals("0", Eval2.eval(code, 1)); + assertEquals("1", Eval2.eval(code, 2)); + assertEquals("0", Eval2.eval(code, 3)); + } + + // Forward ref to 'x' means that + @Ignore @Test + public void testForwardRef1() { + CodeGen code = new CodeGen( +""" +struct S { + { int } f = { -> return x(); }; +}; +val x = { -> return 1; }; +S? s = null; +for(;;) { + if (s) return s.f; +} +"""); + code.parse().opto().typeCheck().GCM(); + assertEquals("return 0;", code._stop.toString()); + assertEquals("0", Eval2.eval(code, 0)); + } + + // Inline hidden called more than once + @Test + public void testInline() { + CodeGen code = new CodeGen( +""" +{int->int}?! i2i = {int i->return i;}; +{{int->int}->{int->int}}! f2f = {{int->int} f->return f;}; +val o = i2i; +if (arg) i2i = null; +if (i2i) return i2i(arg); +return f2f(o)(1); +"""); + code.parse().opto().typeCheck().GCM().localSched(); + assertEquals("Stop[ return Phi(Region,o( arg),o( 1)); return Parm_i(o,int,arg,1); ]", code._stop.toString()); + assertEquals("1", Eval2.eval(code, 2)); + } + + @Test + public void testOperField() { + CodeGen code = new CodeGen( +""" +struct Person { + int coffee_count; +}; +Person !p = new Person; +p.coffee_count += 1; +return p.coffee_count; +"""); + code.parse().opto().typeCheck().GCM().localSched(); + assertEquals("return 1;", code._stop.toString()); + assertEquals("1", Eval2.eval(code, 2)); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter19Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter19Test.java new file mode 100644 index 00000000..a4857a01 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter19Test.java @@ -0,0 +1,398 @@ +package com.seaofnodes.simple; + +import org.junit.Test; +import static org.junit.Assert.*; +import static org.junit.Assert.fail; +import org.junit.Ignore; + +public class Chapter19Test { + + @Test + public void testJig() { + CodeGen code = new CodeGen(""" +return 0; +"""); + code.parse().opto().typeCheck(); + assertEquals("return 0;", code._stop.toString()); + assertEquals("0", Eval2.eval(code, 2)); + } + + + @Test + public void testString() { + CodeGen code = new CodeGen( +""" +struct String { + u8[] cs; + int _hashCode; +}; + +val equals = { String self, String s -> + if( self == s ) return true; + if( self.cs# != s.cs# ) return false; + for( int i=0; i< self.cs#; i++ ) + if( self.cs[i] != s.cs[i] ) + return false; + return true; +}; + +val hashCode = { String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + +val _hashCodeString = { String self -> + int hash=0; + if( self.cs ) { + for( int i=0; i< self.cs#; i++ ) + hash = hash*31 + self.cs[i]; + } + if( !hash ) hash = 123456789; + return hash; +}; + +String !s = new String { cs = new u8[17]; }; +s.cs[0] = 67; // C +s.cs[1] = 108; // l +hashCode(s);"""); + code.parse().opto().typeCheck().GCM().localSched(); + assertEquals("Stop[ return Phi(Region,123456789,Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash)))); return Phi(Region,1,0,0,1); ]", code._stop.toString()); + assertEquals("-2449306563677080489", Eval2.eval(code, 2)); + } + + @Test + public void testBasic0() { + CodeGen code = new CodeGen("return 0;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return 0;", code._stop.toString()); + } + + @Test + public void testBasic1() { + CodeGen code = new CodeGen("return arg+1;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (inc,arg);", code._stop.toString()); + } + + @Test + public void testBasic2() { + CodeGen code = new CodeGen("return -17;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return -17;", code._stop.toString()); + } + + + @Test + public void testBasic3() { + CodeGen code = new CodeGen("return arg==1;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (set==,(cmp,arg));", code._stop.toString()); + } + + @Test + public void testBasic4() { + CodeGen code = new CodeGen("return arg<<1;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (shli,arg);", code._stop.toString()); + } + + @Test + public void testBasic5() { + CodeGen code = new CodeGen("return arg >> 1;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (sari,arg);", code._stop.toString()); + } + + @Test + public void testBasic6() { + CodeGen code = new CodeGen("return arg >>> 1;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (shri,arg);", code._stop.toString()); + } + + @Test + public void testBasic7() { + CodeGen code = new CodeGen("return arg / 2;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (div,arg,2);", code._stop.toString()); + } + + @Test + public void testBasic8() { + CodeGen code = new CodeGen("return arg * 6;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (muli,arg);", code._stop.toString()); + } + + @Test + public void testBasic9() { + CodeGen code = new CodeGen("return arg & 2;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (andi,arg);", code._stop.toString()); + } + + @Test + public void testBasic10() { + CodeGen code = new CodeGen("return arg | 2;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (ori,arg);", code._stop.toString()); + } + + @Test + public void testBasic11() { + CodeGen code = new CodeGen("return arg ^ 2;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (xori,arg);", code._stop.toString()); + } + + @Test + public void testBasic12() { + CodeGen code = new CodeGen("return arg + 2.0;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (addf,(i2f8,arg),2.0f);", code._stop.toString()); + } + + @Test + public void testBasic13() { + CodeGen code = new CodeGen("return arg - 2.0;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (subf,(i2f8,arg),2.0f);", code._stop.toString()); + } + + @Test + public void testBasic14() { + CodeGen code = new CodeGen("return arg * 2.0;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (mulf,(i2f8,arg),2.0f);", code._stop.toString()); + } + + @Test + public void testBasic15() { + CodeGen code = new CodeGen("return arg / 2.0;").parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (divf,(i2f8,arg),2.0f);", code._stop.toString()); + } + + @Test + public void testBasic16() { + CodeGen code = new CodeGen( +""" +int arg1 = arg + 1; +return arg1 / arg;"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (div,(inc,arg),arg);", code._stop.toString()); + } + + @Test + public void testBasic17() { + CodeGen code = new CodeGen( +""" +int arg1 = arg + 1; +return arg1 * arg; +"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (mul,(inc,arg),arg);", code._stop.toString()); + } + + @Test + public void testToFloat() { + CodeGen code = new CodeGen(""" + int a = arg; + return a + 2.0; + """ + ).parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (addf,(i2f8,arg),2.0f);", code._stop.toString()); + } + + @Test + public void testIfStmt() { + CodeGen code = new CodeGen( +""" +int a = 1; +if (arg == 1) + a = arg+2; +else { + a = arg-3; +} +return a;"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return Phi(Region,(addi,arg),(addi,arg));", code.print()); + } + + @Test + public void testIfMerge2() { + CodeGen code = new CodeGen( +""" +int a=arg+1; +int b=arg+2; +if( arg==1 ) + b=b+a; +else + a=b+1; +return a+b;"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (add,(add,Phi(Region,(shli,arg),arg),arg),Phi(Region,4,5));", code.print()); + } + + @Test + public void testLoop() { + CodeGen code = new CodeGen( +""" +int sum=0; +for( int i=0; i<arg; i++ ) + sum += i; +return sum;"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return Phi(Loop,0,(add,Phi_sum,Phi(Loop,0,(inc,Phi_i))));", code.print()); + } + + @Test + public void testAlloc1() { + CodeGen code = new CodeGen( +""" +struct S { int a; S? c; }; +return new S;"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return S;", code.print()); + } + + @Test + public void testLea1() { + CodeGen code = new CodeGen("int x = arg/3; return arg+x+7;"); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (lea,arg,(div,arg,3));", code.print()); + } + + @Test + public void testLea2() { + CodeGen code = new CodeGen("int x = arg/3; return arg+x*4+7;"); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (lea,arg,(div,arg,3));", code.print()); + } + + @Test + public void testLea3() { + CodeGen code = new CodeGen("int x = arg/3; return x*4+arg;"); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return (lea,arg,(div,arg,3));", code.print()); + } + + @Test + public void testAlloc2() { + CodeGen code = new CodeGen("int[] !xs = new int[3]; xs[arg]=1; return xs[arg&1];"); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return .[];", code.print()); + } + + @Test + public void testAlloc3() { + CodeGen code = new CodeGen("int[] !xs = new int[3]; xs[arg]=1; return xs[arg&1]+3;"); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return .[];", code.print()); + } + + @Test + public void testArray1() { + CodeGen code = new CodeGen( +""" +int[] !ary = new int[arg]; +// Fill [0,1,2,3,4,...] +for( int i=0; i<ary#; i++ ) + ary[i] = i; +// Fill [0,1,3,6,10,...] +for( int i=0; i<ary#-1; i++ ) + ary[i+1] += ary[i]; +return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6 +"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return .[];", code.print()); + } + + @Test + public void testArray2() { + CodeGen code = new CodeGen( +""" +flt[] !A = new flt[arg], !B = new flt[arg]; +// Fill [0,1,2,3,4,...] +for( int i=0; i<A#; i++ ) + A[i] = i; +for( int i=0; i<A#; i++ ) + B[i] += A[i]; +"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return 0;", code.print()); + } + + @Test + public void testArray3() { + CodeGen code = new CodeGen( +""" +byte[] !A = new byte[arg]; +for( int i=0; i<A#; i++ ) + A[i]++; +return A[1]; +"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return .[];", code.print()); + } + + @Test + public void testNewton() { + CodeGen code = new CodeGen( +""" +// Newtons approximation to the square root +val sqrt = { flt x -> + flt guess = x; + while( 1 ) { + flt next = (x/guess + guess)/2; + if( next == guess ) return guess; + guess = next; + } +}; +flt farg = arg; +return sqrt(farg); +"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return Phi(Loop,(i2f8,arg),(divf,(addf,(divf,i2f8,Phi_guess),Phi_guess),2.0f));", code.print()); + }; + + + @Test + public void sieveOfEratosthenes() { + CodeGen code = new CodeGen( +""" +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; +"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("return [int];", code.print()); + //assertEquals("int[ 2,3,5,7,11,13,17,19]",Eval2.eval(code, 20)); + } + + + @Test + public void testFcn1() { + CodeGen code = new CodeGen( +""" +val fcn = arg ? { int x -> x*x; } : { int x -> x+x; }; +return fcn(2)*10 + fcn(3); +"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("Stop[ return (add,Phi(Region,{ int -> int #1},{ int -> int #2})( 3),(muli,Phi_( 2))); return (mul,Parm_x($fun,int,3,2),x); return (shli,Parm_x($fun,int,3,2)); ]", code.print()); + } + + @Test + public void testFcn2() { + CodeGen code = new CodeGen( +""" +val sq = { int x -> x*x; }; +return sq(arg) + sq(3); +"""); + code.parse().opto().typeCheck().instSelect("x86_64_v2", "SystemV").GCM().localSched(); + assertEquals("Stop[ return (add,sq(),sq()); return (mul,Parm_x(sq,int,3,arg),x); ]", code.print()); + } +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/chapter20/src/test/java/com/seaofnodes/simple/Chapter20Test.java new file mode 100644 index 00000000..48458f95 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Chapter20Test.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class Chapter20Test { + + @Test + public void testJig() { + CodeGen code = new CodeGen(""" +return 0; +"""); + code.parse().opto().typeCheck(); + assertEquals("return 0;", code._stop.toString()); + assertEquals("0", Eval2.eval(code, 2)); + } +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Eval2.java b/chapter20/src/test/java/com/seaofnodes/simple/Eval2.java new file mode 100644 index 00000000..6ae8427c --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Eval2.java @@ -0,0 +1,448 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.node.*; +import java.util.*; + +public abstract class Eval2 { + + // A mapping from Node ID to evaluation data Object. + // Data Objsts are a boxed Long or Double; or an Object[] of fields, which + // themselves are just more Objects. + private static class Frame { + private static int UID=1; + + private final IdentityHashMap<Node,Object> _data; + private Frame _prior; // Prior frame in this sequence, means when making a new frame we can find old stuff + private int _refcnt; // TODO: Use this in a sentence (free/recycle unused stack frames) + final int _uid = UID++; // Debugging aid + Frame(Frame prior) { + _prior = prior; + if( prior!=null ) prior._refcnt++; + _data = new IdentityHashMap<>(); + _refcnt = 1; + } + + // Dec ref count + void dec() { + assert _refcnt>0; + if( --_refcnt == 0 ) { + Frame prior = _prior; + _prior = null; + _data.clear(); + // TODO: FREE LIST? + if( prior!=null ) + prior.dec(); + } + } + + // Put a mapping from Node->Object + Node put(Node n, Object d) { + assert d!=null; + return put0(n,d); + } + // Put a mapping from Node->Object? + Node put0(Node n, Object d) { + assert _refcnt>0; + _data.put(n,d); + return n; + } + // Get a mapped Node, perhaps using the prior stack frame crawl + Object get(Node n) { + assert _refcnt>0; + Object d = _data.get(n); + return d != null ? d + : _prior == null ? null + : _prior.get(n); + } + + // Short debugging print + public String p() { + return "#"+_uid + (_prior==null ? "" : " >> " + _prior.p()); + } + // Long debugging print + @Override public String toString() { + String s = ""; + if( _prior != null ) s+= _prior+" << "; + s += "#"+_uid+"[ "; + for( Node n : _data.keySet() ) + s += n + ":" + _data.get(n) + ","; + s += "]"; + + return s; + } + } + + // Classic Closure; an environment (stack of frames) and an execution point + private static class Closure { + // Where to return too; either a CallEnd or a Function + final CFGNode _cc; + + // Stack frame at the execution point. Includes Closures which + // recursively include other stack frames + private Frame _frame; + + Closure(CFGNode cc, Frame frame) { + _cc = cc; // One of Fun, CallEnd or Stop (null) + _frame = frame; + assert _frame._refcnt >=0; + _frame._refcnt++; + } + + FunNode fun() { return (FunNode)_cc; } + CallEndNode cend() { return (CallEndNode)_cc; } + boolean isStop() { return _cc instanceof StopNode; } // or Stop + + // + void pushFrame() { + _frame = new Frame(_frame); + _frame._refcnt++; + } + + @Override public String toString() { + return _cc.toString(); + } + } + + // CodeGen has the linker tables + public static CodeGen CODE; + + // Current active Frame, to avoid having to pass it everywhere + public static Frame F; + + public static String eval( CodeGen code, long arg ) { return eval(code,arg,1000); } + public static String eval( CodeGen code, long arg, int timeout ) { + SB trace = null; // new SB(); // TRACE, set to null for off, new SB() for on + // Force local scheduling phase + if( code._phase.ordinal() < CodeGen.Phase.TypeCheck .ordinal() ) code.typeCheck(); + if( code._phase.ordinal() < CodeGen.Phase.Schedule .ordinal() ) code.GCM(); + if( code._phase.ordinal() < CodeGen.Phase.LocalSched.ordinal() ) code.localSched(); + // Set global, so don't have to pass everywhere + CODE = code; + + // Timer for when we run too long. Ticks once per backedge or function call + int loopCnt = 0; + + // Current value for each Node; boxed Long or Double; or an Object[] of + // fields, which themselves are just more Objects. + F = new Frame(null); + + // Start at the START + CFGNode BB = code._start, prior = null; + // Return from main hits Stop + F.put(BB,new Closure(code._stop,F)); + + + // ------------ + // Evaluate until exit or timeout. Each outer loop step computes + // all data nodes under some new Control. + while( true ) { + assert BB!=null; + traceCtrl(BB,trace); + if( trace!=null ) System.out.println(F.p()); + + // Parallel assign Phis + int i = BB instanceof RegionNode r + ? parallelAssignPhis(r,prior,arg,trace) + : 0; + + // Compute the data nodes until BB control + for( ; i < BB.nOuts(); i++ ) + if( !(BB.out(i) instanceof CFGNode) ) + step(BB.out(i), trace); + + // Find the next control target + prior = BB; + BB = BB.uctrl(); // Next unique control target + switch( prior ) { + case StartNode start: F = new Frame(F); break; // Frame for the call to main, as-if Called + case CallNode call : BB = call(call); break; + case IfNode iff : Object p = val(iff.pred()); BB = iff.cproj( p==null || (p instanceof Long x && x==0L) ? 1 : 0); break; + case ReturnNode ret : if( clj(ret.rpc()).isStop() ) return exit(ret); else BB = ret(ret); break; + case FunNode fun : if( loopCnt++ > timeout ) return null; break; // Timeout + case LoopNode loop : if( loopCnt++ > timeout ) return null; break; // Timeout + case CallEndNode cend: break; + case CProjNode cproj: break; + case RegionNode r : break; + default: throw Utils.TODO(); + } + assert !BB.isDead(); + } + } + + + // Phi cache; size is limited to number of Phis at a merge point + private static final Object[] PHICACHE = new Object[256]; + static int parallelAssignPhis(RegionNode r, CFGNode prior, long arg, SB trace) { + + // Compute input path on Phis for this Region + int path = r._inputs.find( prior ); + // Parameters read from prior frame, Phis from local frame + Frame frame = (r instanceof FunNode ? F._prior : F); + boolean isMain = r instanceof FunNode fun && fun.sig().isa(CodeGen.CODE._main); + + // Parallel assign Phis. First parallel read and cache + int i; + for( i = 0; i < r.nOuts(); i++ ) { + if( !(r.out(i) instanceof PhiNode phi) ) break; + if( isMain && phi instanceof ParmNode parm && parm._idx==2 ) + PHICACHE[i] = arg; // Reading the initial arguments to main() + else { + Node n = phi instanceof ParmNode parm + // RPC reads the Call directly; + // Parms may not be linked, so read call args directly + ? (parm._idx==0 ? prior : prior.in(parm._idx)) + // Phis read from path input + : phi.in(path); + PHICACHE[i] = frame.get(n); + } + } + // Parallel assign; might assign before read, so read from cache + for( int j=0; j < i; j++ ) + traceData(F.put0(r.out(j),PHICACHE[j]),trace); + // Return point in basic block past last Phi + return i; + } + + + // Make a call. + private static CFGNode call( CallNode call ) { + // Set up the Return PC and return frame (which is just the current frame). + // Really making a Continuation, but it's just a Closure + Closure cont = new Closure(call.cend(),F); + // Set the return continuation into the current frame RPC + F.put(call, cont ); + // Next Frame to use + F = new Frame(F); + // Target function and environment + return CODE.link(tfp(call.fptr())); + } + + private static CFGNode ret( ReturnNode ret ) { + // Fetch return expression value and continuation from current frame + Object rez = val(ret.expr()); + Closure cont = clj(ret.rpc()); + // Lower Frame reference count; might delete frame + F.dec(); // Lower ref count + // Install replacement Frame; set return result over the CallEnd + F = cont._frame; + F.put0(cont._cc,rez); // Set return value into CallEnd + return cont._cc; // Return CallEnd as the new control + } + + private static String exit( ReturnNode ret ) { + return prettyPrint(ret.expr()._type,val(ret.expr())); + } + + // Take a worklist step: one data op from the current Control is updated. + private static void step(Node n, SB trace) { + if( n instanceof PhiNode ) return; + // Compute new value + F.put0(n,compute(n)); + traceData(n,trace); + // Also do any following projections, which are not in the local schedule otherwise + if( n instanceof MultiNode ) + for( Node use : n._outputs ) + step(use,trace); + } + + // From here down shamelessly copied from Evaluator, written by @Xmilia + private static Object compute( Node n ) { + return switch( n ) { + case AddFNode adf -> d(adf.in(1)) + d(adf.in(2)); + case AddNode add -> x(add.in(1)) + x(add.in(2)); + case AndNode and -> x(and.in(1)) & x(and.in(2)); + case BoolNode.EQF eqf -> d(eqf.in(1)) == d(eqf.in(2)) ? 1L : 0L; + case BoolNode.LEF lef -> d(lef.in(1)) <= d(lef.in(2)) ? 1L : 0L; + case BoolNode.LTF ltf -> d(ltf.in(1)) < d(ltf.in(2)) ? 1L : 0L; + case BoolNode.EQ eq -> Objects.equals(val(eq.in(1)), val(eq.in(2))) ? 1L : 0L; // Bool EQ supports pointers, nil, and integers + case BoolNode.LE le -> x(le .in(1)) <= x(le .in(2)) ? 1L : 0L; + case BoolNode.LT lt -> x(lt .in(1)) < x(lt .in(2)) ? 1L : 0L; + case CastNode cast -> val(cast.in(1)); + case ConstantNode con -> con(con._con); + case DivFNode dvf -> d(dvf.in(2))==0 ? 0D : d(dvf.in(1)) / d(dvf.in(2)); + case DivNode div -> x(div.in(2))==0 ? 0L : x(div.in(1)) / x(div.in(2)); + case LoadNode ld -> load(ld); + case MinusFNode mnf -> - d(mnf.in(1)); + case MinusNode sub -> - x(sub.in(1)); + case MulFNode mlf -> d(mlf.in(1)) * d(mlf.in(2)); + case MulNode mul -> x(mul.in(1)) * x(mul.in(2)); + case NewNode alloc-> alloc(alloc); + case NotNode not -> x(not.in(1)) == 0 ? 1L : 0L; + case OrNode or -> x(or .in(1)) | x(or .in(2)); + case ProjNode proj -> proj._type instanceof TypeMem ? "$mem" : val(proj.in(0)); + case ReadOnlyNode read -> val(read.in(1)); + case SarNode sar -> x(sar.in(1)) >> x(sar.in(2)); + case MemMergeNode merge-> "$mem"; + case ShlNode shl -> x(shl.in(1)) << x(shl.in(2)); + case ShrNode shr -> x(shr.in(1)) >>>x(shr.in(2)); + case StoreNode st -> store(st); + case SubNode sub -> x(sub.in(1)) - x(sub.in(2)); + case SubFNode sbf -> d(sbf.in(1)) - d(sbf.in(2)); + case ToFloatNode toflt-> (double)x(toflt.in(1)); + case XorNode xor -> x(xor.in(1)) ^ x(xor.in(2)); + default -> throw Utils.TODO(); + }; + } + + // Fetch without unboxing, searching up-Frame + static Object val( Node n ) { return F.get(n); } + // Fetch and unbox as primitive long + static long x( Node n ) { Object d = F.get(n); return d==null ? 0 : (Long) F.get(n); } + // Fetch and unbox as primitive double + static double d( Node n ) { Object d = F.get(n); return d==null ? 0 : (Double)F.get(n); } + // Fetch and unbox a function constant + static TypeFunPtr tfp(Node n) { return (TypeFunPtr)F.get(n); } + // Fetch and unbox a closure + static Closure clj(Node n) { return (Closure)F.get(n); } + + // Java box ints and floats. Other things can just be null or some informative string + private static Object con( Type t ) { + return switch ( t ) { + case TypeInteger i -> i.isConstant() ? (Long )i.value() : "INT"; + case TypeFloat f -> f.isConstant() ? (Double)f.value() : "FLT"; + case TypeMemPtr tmp -> tmp; + case TypeFunPtr tfp -> tfp; + case TypeMem mem -> "MEM"; + default -> null; + }; + } + + // Convert array size to array element count + private static int offToIdx( long off, TypeStruct t) { + off -= t.aryBase(); + int scale = t.aryScale(); + long mask = (1L << scale)-1; + assert (off & mask) == 0; + return (int)(off>>scale); + } + + // Builds and returns a pointer Object + private static Object alloc(NewNode alloc) { + TypeStruct type = alloc._ptr._obj; + if( type.isAry() ) { + long sz = (Long)val(alloc.in(1)); + long x = offToIdx(sz, type); + int n = (int)x; + if( n!=x || n<0 ) + throw new NegativeArraySizeException(""+n); + Object[] ary = new Object[n]; // Array body + var elem = type._fields[1]._type; + if (elem instanceof TypeInteger) { + Arrays.fill( ary, 0L ); + } else if (elem instanceof TypeFloat) { + Arrays.fill( ary, 0D ); + } else { + assert elem instanceof TypeMemPtr || elem instanceof TypeFunPtr; + } + return ary; + } + + int num = type._fields.length; + Object[] ptr = new Object[num]; + for( int i=0; i<num; i++ ) + ptr[i] = con(type._fields[i]._type.makeZero()); + return ptr; + } + + private static Object load( LoadNode ld ) { + TypeMemPtr tmp = (TypeMemPtr)ld.ptr()._type; + Object[] fs = (Object[])val(ld.ptr()); + if( ld._name.equals("#") ) + return (long)fs.length; + int idx = tmp._obj.isAry() + ? offToIdx(x(ld.off()),tmp._obj) + : tmp._obj.find(ld._name); + return fs[idx]; + } + + private static Object store( StoreNode st ) { + if( st._name.equals("#") ) + return "$mem"; // Eval stores the length in the java object[], no need to set it now + TypeMemPtr tmp = (TypeMemPtr)st.ptr()._type; + Object[] fs = (Object[])val(st.ptr()); + int idx = tmp._obj.isAry() + ? offToIdx(x(st.off()),tmp._obj) + : tmp._obj.find(st._name); + Object val = val(st.val()); + fs[idx] = val; + return "$mem"; + } + + + // ------------------------------------------------------------------------ + private static final IdentityHashMap<Object,Object> VISIT = new IdentityHashMap<>(); + + + static void traceCtrl( CFGNode BB, SB sb ) { + if( sb==null ) return; + IRPrinter.printLine(BB,sb.p("--- ")); + System.out.print(sb); + sb.clear(); + } + static void traceData( Node n, SB sb ) { + if( sb==null ) return; + IRPrinter.printLine(n,sb).unchar(); + _print( n._type, F.get(n), sb.p('\t'), VISIT ); + System.out.println(sb); + sb.clear(); + VISIT.clear(); + } + + // Use type to print x as a string. Uses a new VISIT table so the debugger + // doesn't try to reuse VISIT if stopping mid-print. The trace code reuses + // VISIT endlessly for some minor speed hope + static String prettyPrint( Type t, Object x ) { return _print(t,x,new SB(), new IdentityHashMap<>()).toString(); } + static SB _print( Type t, Object x, SB sb, IdentityHashMap<Object,Object> visit ) { + if( x instanceof String s ) return sb.p(s); + if( x == null ) return sb.p("null"); + return switch( t ) { + case TypeInteger i -> sb.p( (Long)x); + case TypeFloat f -> sb.p((Double)x); + case TypeMemPtr tmp -> { + if( visit.containsKey(x) ) yield sb.p("$cyclic"); + visit.put(x,x); + // Since never can close the type cycle, without cyclic types, have + // to handle FRefs even here + if( tmp.isFRef() ) + tmp = tmp.makeFrom(((TypeMemPtr)Parser.TYPES.get(tmp._obj._name))._obj); + + Object[] xs = (Object[])x; // Array of fields + if( tmp._obj.isAry() ) { + Type elem = tmp._obj._fields[1]._type; + if( elem == TypeInteger.U8 ) { + // Shortcut u8[] as a String + for( Object o : xs ) + sb.p((char)(long)(Long)o); + } else { + // Array of elements + elem.print(sb).p("[ "); + for( Object o : xs ) + _print( elem, o, sb, visit ).p( "," ); + sb.unchar().p("]"); + } + } else { + sb.p(tmp._obj._name).p("{"); + Field[] flds = tmp._obj._fields; + for( int i=0; i<flds.length; i++ ) + _print( flds[i]._type,xs[i], sb.p(flds[i]._fname).p("="), visit ).p(","); + sb.unchar().p("}"); + } + yield sb; + } + case TypeFunPtr tfp -> sb.p(x.toString()); + case TypeRPC rpc -> sb.p(x.toString()); + case TypeMem mem -> sb.p("$mem"); + case TypeTuple tt -> { + if( tt._types.length>1 && tt._types[1] instanceof TypeMemPtr ) + // Assume a NewNode + yield _print( tt._types[1], x, sb, visit ); + throw Utils.TODO(); + } + case Type tt -> { + if( tt == Type.NIL ) yield sb.p("null"); + throw Utils.TODO(); + } + }; + } +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/FuzzerWrap.java b/chapter20/src/test/java/com/seaofnodes/simple/FuzzerWrap.java new file mode 100644 index 00000000..d67b2065 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/FuzzerWrap.java @@ -0,0 +1,43 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.fuzzer.Fuzzer; +import java.util.Random; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.assertTrue; + +/** + * To use the fuzzer run the <code>fuzzPeeps</code> methods. + * This can be done from IntelliJ Gui or remove the `@Ignore` annotation and start the test case from the command line + * via <code>mvn clean test -Dtest=com.seaofnodes.simple.Fuzzer08Test#fuzzPeeps</code> + */ +public class FuzzerWrap { + + @Ignore + @Test + public void fuzzPeeps() { + var fuzzer = new Fuzzer(); + for (int i=0; i<1000000; i++) + fuzzer.fuzzPeeps(i); + assertTrue(fuzzer.noExceptions()); + } + + + @Test + public void fuzzPeepsSmall() { + Random R = new Random(System.currentTimeMillis()); + var fuzzer = new Fuzzer(); + for (int i=0; i<100; i++) + fuzzer.fuzzPeeps( R.nextLong()); + assertTrue(fuzzer.noExceptions()); + } + + @Test + @Ignore + public void fuzzPeepTiming() { + var fuzzer = new Fuzzer(); + int max_nid=0; + for (int i=0; i<1000000; i++) + max_nid = fuzzer.fuzzPeepTiming(i, max_nid); + } +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/Main.java b/chapter20/src/test/java/com/seaofnodes/simple/Main.java new file mode 100644 index 00000000..523bd306 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/Main.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class Main { + // Compile and run a simple program + public static void main( String[] args ) throws Exception { + // First arg is file, 2nd+ args are program args + String src = Files.readString(Path.of(args[0])); + int arg = Integer.valueOf(args[1]); + CodeGen code = new CodeGen(src); + code.parse().opto().typeCheck().GCM().localSched(); + System.out.println(code._stop); + System.out.println(Eval2.eval(code,arg,100000)); + } +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/MergeSortTest.java b/chapter20/src/test/java/com/seaofnodes/simple/MergeSortTest.java new file mode 100644 index 00000000..d48449d5 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/MergeSortTest.java @@ -0,0 +1,65 @@ +package com.seaofnodes.simple; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class MergeSortTest { + + @Test + public void testMergeSort() { + CodeGen code = new CodeGen( +""" +// based on the top-down version from https://en.wikipedia.org/wiki/Merge_sort + +val merge_sort = { int[] a, int[] b, int n -> + copy_array(a, 0, n, b); + split_merge(a, 0, n, b); +}; + +val split_merge = { int[] b, int begin, int end, int[] a -> + if (end - begin <= 1) + return 0; + int middle = (end + begin) / 2; + split_merge(a, begin, middle, b); + split_merge(a, middle, end, b); + merge(b, begin, middle, end, a); + return 0; +}; + +val merge = { int[] b, int begin, int middle, int end, int[] a -> + int i = begin, j = middle; + + for (int k = begin; k < end; k++) { + // && and || + bool cond = false; + if (i < middle) { + if (j >= end) cond = true; + else if (a[i] <= a[j]) cond = true; + } + if (cond) b[k] = a[i++]; + else b[k] = a[j++]; + } +}; + +val copy_array = { int[] a, int begin, int end, int[] b -> + for (int k = begin; k < end; k++) + b[k] = a[k]; +}; + +int[] !a = new int[arg]; +int[] !b = new int[a#]; + +for (int i = 0; i < a#; i++) + a[i] = a# - i; + +merge_sort(a, b, a#); + +return a; +"""); + code.parse().opto(); + assertEquals("Stop[ return [int]; return 0; ]", code._stop.toString()); + assertEquals("int[ 1,2,3,4,5,6,7,8,9,10,11]", Eval2.eval(code, 11)); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/SchedulerTest.java b/chapter20/src/test/java/com/seaofnodes/simple/SchedulerTest.java new file mode 100644 index 00000000..d50c302a --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/SchedulerTest.java @@ -0,0 +1,73 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.evaluator.Evaluator; +import com.seaofnodes.simple.node.StopNode; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class SchedulerTest { + + private static void assertObj(Object obj, String name, Object... fields) { + assertEquals(Evaluator.Obj.class, obj.getClass()); + var o = (Evaluator.Obj)obj; + assertEquals(name, o.struct()._name); + Assert.assertArrayEquals(fields, o.fields()); + } + + @Test + @Ignore + public void testJig() { + CodeGen code = new CodeGen(""" +struct S { + int f; +} +S !v=new S; +v.f = 2; +int i=v.f; +if (arg) v.f=1; +return i; +"""); + code.parse();//.opto(); + var eval = new Evaluator(code._stop); + assertEquals(2L, eval.evaluate(1, 10)); + } + + @Test + public void testStoreInIf() { + CodeGen code = new CodeGen(""" +struct S { + int f; +}; +S !v0=new S; +if(arg) v0.f=1; +return v0; +"""); + code.parse().opto(); + var eval = new Evaluator(code._stop); + assertObj(eval.evaluate(0, 10), "S", 0L); + assertObj(eval.evaluate(1, 10), "S", 1L); + } + + @Test + public void testStoreInIf2() { + CodeGen code = new CodeGen(""" +struct S { + int f; +}; +S !v=new S; +v.f = 2; +int i=new S.f; +i=v.f; +if (arg) v.f=1; +return i; +"""); + code.parse().opto(); + var eval = new Evaluator(code._stop); + assertEquals(2L, eval.evaluate(0, 10)); + assertEquals(2L, eval.evaluate(1, 10)); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/TypeTest.java b/chapter20/src/test/java/com/seaofnodes/simple/TypeTest.java new file mode 100644 index 00000000..d94f5b5a --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/TypeTest.java @@ -0,0 +1,158 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.type.*; +import org.junit.Assert; +import org.junit.Test; +import static org.junit.Assert.assertSame; + +public class TypeTest { + + // Test basic properties and GLB + @Test + public void testTypeAdHoc() { + Assert.assertEquals( Type.BOTTOM, TypeInteger.TRUE.meet(TypeNil.NIL) ); + Assert.assertEquals( Type.BOTTOM, TypeInteger.TOP.meet(TypeNil.NIL) ); + + + TypeStruct s1 = TypeStruct.make("s1", new Field[]{ + Field.make("a", TypeInteger.BOT,-1, false), + Field.make("b", TypeInteger.BOT,-2, false) }); + TypeStruct s2 = TypeStruct.make("s2", new Field[]{ + Field.make("a", TypeInteger.BOT,-3, false), + Field.make("b", TypeInteger.BOT,-4, false) }); + Assert.assertEquals(s1.makeRO(), s1.glb()); + Assert.assertNotEquals(s1, s1.dual()); + Assert.assertEquals(s1.makeRO(), s1.dual().glb()); + + TypeMem m1 = TypeMem.make(1,TypeNil.NIL); + TypeMem m2 = TypeMem.make(2,TypeInteger.U16); + TypeMem m3 = TypeMem.make(3,TypeFloat.F64); + TypeMem m4 = TypeMem.make(4,TypeInteger.BOT); + + Assert.assertNotEquals(m1, m2); + Assert.assertNotEquals(m2, m3); + Assert.assertNotEquals(m3, m4); + + Assert.assertEquals(TypeStruct.BOT, s1.meet(s2)); + Assert.assertEquals(TypeMem .BOT, m1.meet(m2)); + Assert.assertEquals(TypeMem.make(0,Type.BOTTOM), m1.meet(m3)); + Assert.assertEquals(TypeMem .BOT, m3.meet(m4)); + + Assert.assertEquals(TypeMem.make(1,Type.BOTTOM), m1.glb()); + Assert.assertEquals(TypeMem.make(1,Type.XNIL), m1.dual()); + Assert.assertEquals(m4.dual(), m4.glb().dual()); + + TypeMemPtr ptr1 = TypeMemPtr.make(s1); + Assert.assertEquals(s1, ptr1._obj); + TypeMemPtr ptr2 = TypeMemPtr.make(s2); + Assert.assertEquals(s2, ptr2._obj); + + TypeMemPtr ptr1nil = TypeMemPtr.makeNullable(s1); + Assert.assertEquals(s1, ptr1nil._obj); + Assert.assertTrue( ptr1nil.nullable() ); + Assert.assertNotNull( ptr1nil._obj ); + TypeMemPtr ptr2nil = TypeMemPtr.makeNullable( s2); + Assert.assertEquals(s2, ptr2nil._obj); + + Assert.assertNotEquals(ptr1, ptr2); + Assert.assertNotEquals(ptr1, ptr1.glb()); + Assert.assertEquals(ptr1nil.makeRO(), ptr1.glb()); + + Assert.assertEquals(ptr1, ptr1.dual().dual()); + Assert.assertEquals(ptr1.glb().makeRO(), ptr1.dual().glb()); + Assert.assertEquals(TypeMemPtr.makeNullable(TypeStruct.BOT), ptr1.meet(ptr2nil)); + Assert.assertEquals(ptr1.glb(), ptr1.meet(TypeNil.NIL).makeRO()); + + TypeMemPtr TOP = TypeMemPtr.TOP; + TypeMemPtr BOT = TypeMemPtr.makeNullable(TypeStruct.BOT); + TypeMemPtr PTR = TypeMemPtr.make(TypeStruct.BOT); + Type NULL = TypeNil.NIL; + Type PTR_meet_NULL = NULL.meet(PTR); + Assert.assertEquals(BOT, PTR_meet_NULL); + Type ptr1_meet_ptr2 = ptr1.meet(ptr2); + Assert.assertEquals(PTR, ptr1_meet_ptr2); + Type NULL_join_ptr1 = NULL.join(ptr1); + Assert.assertEquals(TypePtr.XNPTR, NULL_join_ptr1); + Type NULL_join_PTR = PTR.join(NULL); + Assert.assertEquals(TypePtr.XNPTR, NULL_join_PTR); + Type ptr1_dual = ptr1.dual(); + Type nullableptr1_dual = ptr1nil.dual(); + } + + // Test theoretical properties. + // This is a symmetric complete bounded (ranked) lattice. + // Also, the meet is commutative and associative. + // The lattice has a dual (symmetric), and join is ~(~x meet ~y). + // See https://en.wikipedia.org/wiki/Lattice_(order). + @Test + public void testLatticeTheory() { + Type[] ts = Type.gather(); + + // Confirm commutative & complete + for( Type t0 : ts ) + for( Type t1 : ts ) { + check_commute (t0,t1); + check_symmetric(t0,t1); + } + + // Confirm associative + for( Type t0 : ts ) + for( Type t1 : ts ) + for( Type t2 : ts ) + assoc(t0,t1,t2); + + // Confirm symmetry. If A isa B, then A.join(C) isa B.join(C) + for( Type t0 : ts ) + for( Type t1 : ts ) + if( t0.isa(t1) ) + for( Type t2 : ts ) { + Type t02 = t0.join(t2); + Type t12 = t1.join(t2); + Type mt = t02.meet(t12); + assertSame(mt,t12); + } + } + + // By design in meet, args are already flipped to order _type, which forces + // symmetry for things with badly ordered _type fields. The question is + // still interesting for other orders. + private static void check_commute( Type t0, Type t1 ) { + if( t0==t1 ) return; + if( t0.is_simple() && !t1.is_simple() ) return; // By design, flipped the only allowed order + Type mta = t0.meet(t1); + Type mtb = t1.meet(t0); // Reverse args and try again + assertSame(mta,mtb); + } + + // A & B = MT + // Expect: ~A & ~MT == ~A + // Expect: ~B & ~MT == ~B + private static void check_symmetric( Type t0, Type t1 ) { + if( t1==t0 ) return; + Type mt = t0.meet(t1); + Type dm = mt.dual(); + Type d0 = t0.dual(); + Type d1 = t1.dual(); + Type ta = dm.meet(d1); + Type tb = dm.meet(d0); + assertSame(ta,d1); + assertSame(tb,d0); + } + + private static void assoc( Type t0, Type t1, Type t2 ) { + Type t01 = t0 .meet(t1 ); + Type t12 = t1 .meet(t2 ); + Type t01_2 = t01.meet(t2 ); + Type t0_12 = t0 .meet(t12); + assertSame(t01_2,t0_12); + } + + // Test cyclic types and meets + @Test + public void testCyclic0() { + Type d0 = TypeStruct.S1.dual(); + Type d1 = d0.dual(); + assertSame(TypeStruct.S1,d1); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java b/chapter20/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java new file mode 100644 index 00000000..a3b87e90 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java @@ -0,0 +1,383 @@ +package com.seaofnodes.simple.evaluator; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + +import java.util.*; + +public class Evaluator { + + private static final Object MEMORY = new Object(); + + public record Obj(TypeStruct struct, Object[] fields) { + private void init(IdentityHashMap<Obj, Integer> objs) { + var oid = objs.get(this); + if (oid != null) { + if (oid != 0) objs.put(this, 0); + return; + } + objs.put(this, -1); + for (var obj : fields) { + if (obj instanceof Obj o) o.init(objs); + } + } + private static int p(StringBuilder sb, IdentityHashMap<Obj, Integer> objs, int id, String indentation, String step, String sep, Object obj) { + if (obj instanceof Obj o) return o.p(sb, objs, id, indentation, step, sep); + sb.append(obj); + return id; + } + private int p(StringBuilder sb, IdentityHashMap<Obj, Integer> objs, int id, String indentation, String step, String sep) { + var cid = objs.get(this); + assert cid != null; + if (cid > 0) { + sb.append("obj@").append(cid); + return id; + } + if (struct._name.equals("[u8]")) { + sb.append('"'); + for (int i=struct._fields.length-1; i<fields.length; i++) { + var v = fields[i]; + if (v == null) v = 0; + assert v instanceof Number; + var n = ((Number)v).byteValue() & 0xFF; + if (n >= 0x20 && n < 0x80) { + if (n == 0x22 || n == 0x5C) sb.append("\\"); + sb.append((char)n); + } else { + sb.append("\\x").append("0123456789abcdef".charAt(n >> 4)).append("0123456789abcdef".charAt(n & 0xF)); + } + } + sb.append('"'); + return id; + } + sb.append("Obj<").append(struct._name).append(">"); + if (cid == 0) { + cid = id++; + objs.put(this, cid); + sb.append("@").append(cid); + } + sb.append("{"); + if (struct._fields.length == 0) { + assert !struct.isAry(); + sb.append("}"); + return id; + } + String nextIndent = indentation + step; + int e = struct._fields.length - 1; + for(int i=0; i<e; i++) { + sb.append(nextIndent).append(struct._fields[i]._fname).append("="); + id = p(sb, objs, id, nextIndent, step, sep, fields[i]); + sb.append(sep); + } + sb.append(nextIndent).append(struct._fields[e]._fname).append("="); + if (struct.isAry()) { + sb.append("["); + if (fields.length > e) { + String innerIndent = nextIndent + step; + sb.append(innerIndent); + id = p(sb, objs, id, nextIndent, step, sep, fields[e]); + for (int i = e+1; i < fields.length; i++) { + sb.append(sep).append(innerIndent); + id = p(sb, objs, id, nextIndent, step, sep, fields[i]); + } + sb.append(nextIndent); + } + sb.append("]"); + } else { + id = p(sb, objs, id, nextIndent, step, sep, fields[e]); + } + sb.append(indentation).append("}"); + return id; + } + private StringBuilder p(StringBuilder sb, String indentation, String step, String sep) { + var objs = new IdentityHashMap<Obj, Integer>(); + init(objs); + p(sb, objs, 1, indentation, step, sep); + return sb; + } + @Override + public String toString() { + if (struct._name.equals("[u8]")) { + var sb = new StringBuilder(); + for (int i=struct._fields.length-1; i<fields.length; i++) { + var v = fields[i]; + if (v == null) v = 0; + assert v instanceof Number; + var n = ((Number)v).byteValue() & 0xFF; + sb.append((char)n); + } + return sb.toString(); + } + return p(new StringBuilder(), "", "", ",").toString(); + } + public String pretty() { + return p(new StringBuilder(), "\n", " ", "").toString(); + } + } + + private static int getFieldIndex(TypeStruct struct, MemOpNode memop, long off) { + int idx = struct.find(memop._name); + if( idx >= 0 ) return idx; + throw new AssertionError("Field "+memop._name+" not found in struct " + struct._name); + } + + public enum Status { + TIMEOUT, + FALLTHROUGH + } + + /** + * Find the start node from some node in the graph or null if there is no start node + */ + private static StartNode findStart(BitSet visit, Node node) { + if (node == null) return null; + if( visit.get(node._nid) ) return null; + visit.set(node._nid); + StartNode start = node instanceof StartNode ? (StartNode) node : null; + for( Node def : node._inputs ) { + var res = findStart(visit,def); + if( res != null ) start = res; + } + for( Node use : node._outputs ) { + var res = findStart(visit,use); + if( res != null ) start = res; + } + return start; + } + + private final Object[] values; + private final ArrayList<Object> phiCache = new ArrayList<>(); + private final StartNode start; + private final Scheduler.Block startBlock; + + public Evaluator(Node graph) { + var visited = new BitSet(); + this.start = findStart(visited, graph); + if (this.start == null) { + this.values = null; + this.startBlock = null; + return; + } + this.startBlock = Scheduler.schedule(this.start); + this.values = new Object[visited.length()]; + } + + private long div(DivNode div) { + long in2 = vall(div.in(2)); + return in2 == 0 ? 0 : vall(div.in(1)) / in2; + } + private double div(DivFNode div) { + double in2 = vald(div.in(2)); + return in2 == 0 ? 0 : vald(div.in(1)) / in2; + } + + private static long offToIdx(long off, TypeStruct t) { + off -= t.aryBase(); + int scale = t.aryScale(); + long mask = (1L << scale)-1; + assert (off & mask) == 0; + return off>>scale; + } + + private Object alloc(NewNode alloc) { + TypeStruct type = alloc._ptr._obj; + Object[] body=null; + int num; + if( type.isAry() ) { + long sz = (Long)val(alloc.in(1)); + long n = offToIdx(sz, type); + if( n < 0 ) + throw new NegativeArraySizeException(""+n); + body = new Object[(int)n+1]; // Array body + var elem = type._fields[1]._type; + if (elem instanceof TypeInteger) { + for (int i=0; i<n; i++) body[i+1] = 0L; + } else if (elem instanceof TypeFloat) { + for (int i = 0; i < n; i++) body[i+1] = 0D; + } else { + assert elem instanceof TypeMemPtr; + } + // Length value + body[0] = vall(alloc.in(2+2)); + } else { + body = new Object[num = type._fields.length]; + for( int i=0; i<num; i++ ) + body[i] = switch( alloc._ptr._obj._fields[i]._type ) { + case TypeInteger ti -> 0L; + case TypeFloat tf -> 0; + default -> null; + }; + } + Object[] mems = new Object[type._fields.length+2]; + // mems[0] is control + mems[1] = new Obj(type,body); // the ref + // mems[2+...] are memory aliases + return mems; + } + + private Object load(LoadNode load) { + var from = (Obj)val(load.ptr()); + var off = vall(load.off()); + var idx = getFieldIndex(from.struct, load, (Long)off); + if( idx==from.struct._fields.length-1 && from.struct.isAry() ) { + long len = from.fields.length - from.struct._fields.length + 1; + long i = offToIdx(off, from.struct); + if( i < 0 || i >= len ) + throw new ArrayIndexOutOfBoundsException("Array index out of bounds " + i + " < " + len); + return from.fields[(int)i+from.struct._fields.length-1]; + + } else + return from.fields[idx]; + } + + private Object store(StoreNode store) { + var to = (Obj)val(store.ptr()); + var off = vall(store.off()); + var val = val(store.val()); + var idx = getFieldIndex(to.struct, store, (Long)off); + if( idx==to.struct._fields.length-1 && to.struct.isAry() ) { + long len = to.fields.length - to.struct._fields.length + 1; + long i = offToIdx(off, to.struct); + if( i < 0 || i >= len ) + throw new RuntimeException("Array index out of bounds " + off + " <= " + len); + to.fields[(int)i+to.struct._fields.length-1] = val; + } else + to.fields[idx] = val; + return null; + } + + private static boolean isTrue(Object obj) { + if (obj == null) return false; + if (obj instanceof Number n) return n.longValue() != 0; + return true; + } + + private Object val(Node node) { + return values[node._nid]; + } + + private long vall(Node node) { + var v = val(node); + if (v instanceof Number n) return n.longValue(); + throw new AssertionError("Not a long " + v); + } + private double vald(Node node) { + var v = val(node); + if (v instanceof Number n) return n.doubleValue(); + if( v == null ) return 0; + throw new AssertionError("Not a double " + v); + } + + private Object cons(ConstantNode cons) { + var type = cons.compute(); + if (type instanceof TypeInteger i) return i.isConstant() ? i.value() : Integer.MAX_VALUE; + if (type instanceof TypeFloat i) return i.value(); + return null; + } + + private Object exec(Node node) { + return switch (node) { + case ConstantNode cons -> cons(cons); + case AddNode add -> vall(add.in(1)) + vall(add.in(2)); + case AddFNode add -> vald(add.in(1)) + vald(add.in(2)); + case BoolNode.EQF eq -> vald(eq.in(1)) == vald(eq.in(2)) ? 1L : 0L; + case BoolNode.LEF le -> vald(le.in(1)) <= vald(le.in(2)) ? 1L : 0L; + case BoolNode.LTF lt -> vald(lt.in(1)) < vald(lt.in(2)) ? 1L : 0L; + case BoolNode.EQ eq -> Objects.equals(val(eq.in(1)), val(eq.in(2))) ? 1L : 0L; + case BoolNode.LE le -> vall(le.in(1)) <= vall(le.in(2)) ? 1L : 0L; + case BoolNode.LT lt -> vall(lt.in(1)) < vall(lt.in(2)) ? 1L : 0L; + case DivNode div -> div(div); + case DivFNode div -> div(div); + case MinusNode minus -> -vall(minus.in(1)); + case MinusFNode minus -> -vald(minus.in(1)); + case MulNode mul -> vall(mul.in(1)) * vall(mul.in(2)); + case MulFNode mul -> vald(mul.in(1)) * vald(mul.in(2)); + case NotNode not -> isTrue(val(not.in(1))) ? 0L : 1L; + case SubNode sub -> vall(sub.in(1)) - vall(sub.in(2)); + case SubFNode sub -> vald(sub.in(1)) - vald(sub.in(2)); + case ShlNode shl -> vall(shl.in(1)) << vall(shl.in(2)); + case ShrNode shr -> vall(shr.in(1)) >>> vall(shr.in(2)); + case SarNode sar -> vall(sar.in(1)) >> vall(sar.in(2)); + case AndNode and -> vall(and.in(1)) & vall(and.in(2)); + case OrNode or -> vall(or .in(1)) | vall(or .in(2)); + case XorNode xor -> vall(xor.in(1)) ^ vall(xor.in(2)); + case CastNode cast -> val(cast.in(1)); + case ToFloatNode cast -> (double)vall(cast.in(1)); + case LoadNode load -> load(load); + case StoreNode store -> store(store); + case NewNode alloc -> alloc(alloc); + case CProjNode cproj -> ((Object[])val(cproj.ctrl()))[cproj._idx]; + case ProjNode proj -> ((Object[])val( proj.in(0) ))[ proj._idx]; + case MemMergeNode mem -> null; + case ReadOnlyNode ro -> val(ro.in(1)); + case CallNode call -> Utils.TODO(); // should not reach here, go the IfNode route + default -> throw new AssertionError("Unexpected node " + node); + }; + } + + /** + * Run the graph until either a return is found or the number of loop iterations are done. + */ + public Object evaluate(long parameter, int loops) { + if (start == null) return Status.TIMEOUT; + var s = new Object[start.compute()._types.length]; + s[1] = MEMORY; + s[2] = parameter; + values[start._nid] = s; + int i=0; + Scheduler.Block block = this.startBlock; + for(;;) { + for(; i<block.nodes().length;i++) values[block.nodes()[i]._nid] = exec(block.nodes()[i]); + i=0; + switch (block.exit()) { + case null: + return Status.FALLTHROUGH; + case ReturnNode ret : return val(ret .expr()); + case IfNode ifn: + block = block.next()[isTrue(val(ifn.in(1))) ? 0 : 1]; + if (block == null) return Status.FALLTHROUGH; + break; + case RegionNode region: + if (loops-- <= 0) return Status.TIMEOUT; + int exit = block.exitId(); + assert exit > 0 && region.nIns() > exit; + block = block.next()[0]; + assert block != null; + for (; i < block.nodes().length; i++) { + if (!(block.nodes()[i] instanceof PhiNode phi)) break; + var val = region instanceof FunNode fun && "main".equals(fun._name) && ((ParmNode)phi)._idx==2 && exit==1 + ? parameter + : val(phi.in(exit)); + phiCache.add(val); + } + for (i=0; i<phiCache.size(); i++) { + values[block.nodes()[i]._nid] = phiCache.get(i); + } + phiCache.clear(); + break; + default: + throw Utils.TODO("Unexpected control node " + block.exit()); + } + } + } + + public static Object evaluate(StopNode graph) { + return evaluate(graph, 0); + } + + public static Object evaluate(StopNode graph, long parameter) { + return evaluate(graph, parameter, 1000); + } + + public static Object evaluate(StopNode graph, long parameter, int loops) { + var res = evaluateWithResult(graph, parameter, loops); + if (res == Status.TIMEOUT) throw new RuntimeException("Timeout"); + return res; + } + + public static Object evaluateWithResult(StopNode graph, long parameter, int loops) { + return new Evaluator(graph).evaluate(parameter, loops); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java b/chapter20/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java new file mode 100644 index 00000000..4bfa7628 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java @@ -0,0 +1,606 @@ +package com.seaofnodes.simple.evaluator; + +import com.seaofnodes.simple.Utils; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; + +import java.util.*; + +/** + * A late scheduler for test cases. + * <br> + * <br> + * This scheduler is used for tests and is not the final scheduler. + * <br> + * <br> + * The scheduler schedules not as late as possible. Since it is just for test cases this is fine. + * A better scheduler will try to place nodes outside of loops. Due to the schedule late strategy + * this scheduler might place initialization code in loops. + * <br> + * We schedule late here since memory edges have some implicit information. There can only be one + * memory edge (for a field) at a time. However, before <code>if</code> statements this might split. + * When scheduling early a store might be moved out of the loop but then there are two edges alive. + * The one through the store node and the adjacent one. This is an invalid schedule and instead of + * handling this case a late scheduler does not have this problem as memory edges are combined + * through phi nodes. + * <br> + * <br> + * A bit about this scheduler. It first discovers all alive nodes and allocates side data for them. + * Then the control flow graph is build. Then, all phis are placed and finally all the remaining nodes + * in an order where a node is placed when all alive uses are placed. Finally, the output is produced. + */ +public class Scheduler { + + /** + * A basic block with a schedule of the containing nodes. + * + * @param nodes The nodes in the final schedule of this block + * @param exit The exit node of this block. + * @param exitId In case the next block is a region this gives the entry id into the region. + * @param next The next blocks. + */ + public record Block(Node[] nodes, Node exit, int exitId, Block[] next) {} + + /** + * Definition of a block in the building process. + */ + private static class BasicBlock { + + /** + * Dominator of this block + */ + final BasicBlock dom; + /** + * Id of this block where <code>a.depth < b.depth</code> means that a is before b or + * both are in different branches. + */ + final int depth; + /** + * Previous blocks + */ + final BasicBlock[] prev; + /** + * Entry to this block + */ + final Node entry; + /** + * Schedules node in reverse order. + */ + final ArrayList<Node> reverseSchedule = new ArrayList<>(); + + /** + * Initialize the block. + * @param entry The node that started this block + * @param depth The depth of this node. All previous nodes need to have a lower value. + * @param prev The previous blocks. + */ + BasicBlock(Node entry, int depth, BasicBlock... prev) { + BasicBlock dom = null; + int max = -1; + for(var bb: prev) { + max = Math.max(max, bb.depth); + dom = dom==null?bb:dom(dom, bb); + } + assert depth > max; + this.dom = dom; + this.depth = depth; + this.entry = entry; + this.prev = prev; + } + BasicBlock(Node entry, BasicBlock prev) { + this(entry,prev.depth+1,prev); + } + + } + + /** + * Ancillary data for nodes used during the scheduling process. + */ + private static class NodeData { + + /** + * The node this ancillary data is used for. + */ + final Node node; + + /** + * Number of alive users not yet scheduled. + */ + int users = 1; + + /** + * Block into which the node should be placed so far. + * This will be updated with every placed use. + */ + BasicBlock block; + + /** + * Initialize ancillary data for a node + * @param node The node this ancillary data is associated with. + */ + NodeData(Node node) { + this.node = node; + } + + } + + /** + * Ancillary data for nodes. + */ + private final IdentityHashMap<Node, NodeData> data = new IdentityHashMap<>(); + /** + * List of nodes which can be placed since all users are placed. + */ + private final Stack<NodeData> scheduleQueue = new Stack<>(); + + /** + * Get the ancillary data for an alive node. + * @param node The node for which the ancillary data is requested. + * @return The ancillary data for the node. + */ + NodeData d(Node node) { + return od(node).orElseThrow(AssertionError::new); + } + + /** + * Get the ancillary data for a potentially dead node. + * @param node The node for which the ancillary data is requested. + * @return The ancillary data for the node an alive node or + * <code>Optional.empty()</code> for <code>null</code> or dead nodes. + */ + Optional<NodeData> od(Node node) { + return Optional.ofNullable(data.get(node)); + } + + /** + * Checks a node for validity + * @param data The node to check. + * @return true if all placed inputs are before this node. + */ + private boolean isValid(NodeData data) { + return data.node._inputs.stream() + .map(i->i==null?null:d(i)) + .map(d->d==null||d.users>0?null:d.block) + .allMatch(d->d==null||dom(d, data.block)==d); + } + + /** + * Checks that node is not XCtrl. + * @param node The node to check. + * @return true if the node is not XCtrl. + */ + private static boolean isNotXCtrl(Node node) { + return !(node instanceof ConstantNode c && c.compute() == Type.XCONTROL) && !(node instanceof XCtrlNode); + } + + /** + * Return the dominator of <code>a</code> and <code>b</code> + * @param a Block a + * @param b Block b + * @return The dominator of <code>a</code> and <code>b</code> + */ + private static BasicBlock dom(BasicBlock a, BasicBlock b) { + while (a!=b) { + if (a.depth >= b.depth) a=a.dom; + if (b.depth > a.depth) b=b.dom; + } + return a; + } + + /** + * Return if a is before b. + * @param a Block a + * @param b Block b + * @return true is a is before b. + */ + private static boolean isBefore(BasicBlock a, BasicBlock b) { + while (b.depth > a.depth) { + while (b.dom.depth > a.depth) b = b.dom; + for (int i=0; i<b.prev.length-1; i++) { + if (isBefore(a, b.prev[i])) return true; + } + b = b.prev[b.prev.length-1]; + } + return a == b; + } + + /** + * Is node placed during the control flow graph build. + * @param data Node to check. + * @return true if node is placed during the control flow graph build. + */ + private static boolean isPinnedNode(NodeData data) { + return data.node instanceof CFGNode || data.node instanceof PhiNode; + } + + /** + * Refine placement of a node. + * @param data The node to refine. + * @param block The block before the node should be scheduled. + */ + private void refinePlacement(NodeData data, BasicBlock block) { + assert !isPinnedNode(data); + data.block = data.block == null ? block : dom(data.block, block); + assert isValid(data); + } + + /** + * Refine placement if before is happening before data + * @param data The node for which the placement should be refined. + * @param before The node before which the data node should be placed. + */ + private void optionalRefinePlacement(NodeData data, Node before) { + od(before).ifPresent(d->{ + // before might be in a different branch. So check this case with isBefore + // and only refine the placement if it is not in a different branch. + if (isBefore(d.block, data.block)) refinePlacement(data, d.block); + }); + } + + /** + * Schedule all nodes not yet scheduled. + */ + private void doSchedule() { + while (!scheduleQueue.isEmpty()) { + var data = scheduleQueue.pop(); + assert !isPinnedNode(data); + + assert data.block != null; + assert data.users == 0; + + if (data.node instanceof LoadNode l) { + assert isValid(data); + // Handle anti-deps of load nodes. + // At this point all anti-dep nodes are scheduled, + // but they did not refine the placement + // so do that now. + var mem = l.in(1); + for(var out : mem._outputs) { + if (out instanceof PhiNode p) { + var r = p.in(0); + for (int i = 1; i < p.nIns(); i++) { + if (p.in(i) == mem) optionalRefinePlacement(data, r.in(i)); + } + } else if (!(out instanceof LoadNode)) { + optionalRefinePlacement(data, out); + } + } + } + + assert isValid(data); + data.block.reverseSchedule.add(data.node); + + for(var in:data.node._inputs) { + if (in!=null) update(d(in), data.block); + } + if (data.node instanceof StoreNode s) { + // Store nodes have anti-deps to load nodes. + // So decrease the uses of these loads when the store is placed. + for (var out: s.in(1)._outputs) { + if (out instanceof LoadNode) od(out).ifPresent(this::decUsers); + } + } + } + + // Now all nodes should be placed and have a block assigned + assert this.data.values().stream().noneMatch(d->d.block==null); + } + + /** + * Decrement the not-yet-placed users of a block. + * @param data The node for which the users should be decremented. + */ + private void decUsers(NodeData data) { + assert data.users > 0; + if (--data.users == 0) { + assert data.block != null; + assert isValid(data); + + // When all users are gone this node can be scheduled. + scheduleQueue.push(data); + } + } + + /** + * Update the placement of a node + * @param data The node to update + * @param block The block before which the node should happen. + */ + private void update(NodeData data, BasicBlock block) { + assert block != null; + if (data.users == 0) { + assert isPinnedNode(data); + return; + } + refinePlacement(data, block); + decUsers(data); + } + + /** + * Checks if a CFG node is ready to be placed. This is used for regions joining branches. + * @param node The CFG node to check + * @return true if all parents of a CFG node are placed. + */ + private boolean isCFGNodeReady(Node node) { + if (node instanceof LoopNode l) { + assert d(l.in(1)).block != null; + } else if (node instanceof RegionNode r) { + for(int i=1; i<r.nIns(); i++) { + if (od(r.in(i)).map(d->d.block==null).orElse(false)) return false; + } + } else if (node instanceof XCtrlNode) { + return false; + } else { + assert d(node.in(0)).block != null; + } + return true; + } + + /** + * Schedule all phi nodes. + * @param phiQueue List of all the phi nodes to schedule. + */ + private void schedulePhis(Stack<NodeData> phiQueue) { + while(!phiQueue.empty()) { + var data = phiQueue.pop(); + var phi = (PhiNode) data.node; + var r = phi.in(0); + data.block = d(r).block; + for(int i=1; i<phi.nIns(); i++) { + var in = phi.in(i); + od(r.in(i)).ifPresent(d->update(d(in), d.block)); + } + } + } + + /** + * Build the control flow graph. + * @param start The start node. + */ + private void doBuildCTF(StartNode start) { + var queue = new Stack<NodeData>(); + var phiQueue = new Stack<NodeData>(); + queue.push(d(start)); + while (!queue.isEmpty()) { + var data = queue.pop(); + var node = data.node; + assert node instanceof CFGNode; + BasicBlock block = switch (node) { + case StartNode s -> new BasicBlock(s, 0); + case LoopNode l -> new BasicBlock(l, d(l.in(1)).block); + case RegionNode r -> { + var prev = new ArrayList<BasicBlock>(); + int depth = 0; + for(int i=1; i<r.nIns(); i++) { + if( !od(r.in(i)).isPresent() ) continue; + BasicBlock bb = d(r.in(i)).block; + prev.add(bb); + depth = Math.max(depth,bb.depth); + } + yield new BasicBlock(r, depth+1, prev.toArray(BasicBlock[]::new)); + } + case IfNode i -> d(i.in(0)).block; + case ReturnNode ret -> d(ret .in(0)).block; + case CallNode call -> throw Utils.TODO(); + default -> new BasicBlock(node, d(node.in(0)).block); + }; + data.block = block; + if (node instanceof RegionNode r) { + // Regions might have phis which need to be scheduled. + // Put them on a list for later scheduling. + for (var out:r._outputs) { + if (out instanceof PhiNode p) { + var d = this.data.get(p); + if (d!=null) { + d.users=0; + phiQueue.push(d); + } + } + } + } + if( node instanceof FunNode fun ) { + // Cannot find CFG user from fun via a naive search, because always find + // Return but Return is not always the one CFG user. + CFGNode n = fun.uctrl(); + assert isCFGNodeReady(n) && d(n).block==null; + queue.push(d(n)); + } else if( !(node instanceof ReturnNode) ) { + for (Node n : node._outputs) + if(n instanceof CFGNode && isCFGNodeReady(n) && d(n).block == null) + queue.push(d(n)); + } + var b = block; + for(var in:data.node._inputs) od(in).ifPresent(d->update(d, b)); + } + + // Schedule all phi nodes. + schedulePhis(phiQueue); + } + + /** + * Mark a node alive and create ancillary data for it. + * @param queue List into which this node should be placed if it needs to be visited. + * @param node Node which should be marked alive. + * @param cfg If this node is a CFG node. + */ + private void markAlive(Stack<NodeData> queue, Node node, boolean cfg) { + var nd = od(node).orElse(null); + if (nd != null) { + // Node was already visited, just increase the users. + if (nd.users>0) nd.users++; + return; + } + assert node instanceof CFGNode == cfg; + assert isNotXCtrl(node); + nd = new NodeData(node); + if (cfg) nd.users=0; + data.put(node, nd); + queue.push(nd); + } + + /** + * Mark all alive nodes. + * First all CFG nodes are marked, then all other nodes. + * @param node THe start node. + */ + private void doMarkAlive(Node node) { + var cfgQueue = new Stack<NodeData>(); + var dataQueue = new Stack<NodeData>(); + var mem = new Stack<NodeData>(); + markAlive(cfgQueue, node, true); + // Mark all CFG nodes. + while (!cfgQueue.isEmpty()) { + var data = cfgQueue.pop(); + node = data.node; + assert node instanceof CFGNode; + if (!(node instanceof ReturnNode) ) { + for (var out : node._outputs) if (out!=null && out instanceof CFGNode && isNotXCtrl(out)) markAlive(cfgQueue, out, true); + } + for (var in : node._inputs) if(in!=null && !(in instanceof CFGNode) && isNotXCtrl(in)) markAlive(dataQueue, in, false); + } + // Mark all other nodes. + while (!dataQueue.isEmpty()) { + var data = dataQueue.pop(); + node = data.node; + assert !(node instanceof CFGNode); + if (node instanceof PhiNode phi) { + var r = phi.in(0); + for (int i=1; i<phi.nIns(); i++) { + if (od(r.in(i)).isPresent()) markAlive(dataQueue, phi.in(i), false); + } + } else { + for (var in : node._inputs) if (in != null && !(in instanceof CFGNode)) markAlive(dataQueue, in, false); + } + if (node instanceof StoreNode) mem.push(data); + } + // Handle store nodes and increase load with an anti-dep to the store. + while (!mem.isEmpty()) { + var data = mem.pop(); + node = data.node; + for(var out:node.in(1)._outputs) { + if (out instanceof LoadNode) od(out).ifPresent(d->d.users++); + } + } + } + + /** + * Helper function to append the nodes of a block to an array of nodes + * @param arr The array the nodes of a block should be appended to. + * @param node A potentially separator node + * @param block The block from which the nodes should be appended. + * @return The combined new array. + */ + private static Node[] appendNodes(Node[] arr, Node node, BasicBlock block) { + var idx = arr.length; + arr = Arrays.copyOf(arr, arr.length+block.reverseSchedule.size()+(node==null?0:1)); + if (node != null) arr[idx++] = node; + for(int i=block.reverseSchedule.size()-1;i>=0;i--) { + arr[idx++] = block.reverseSchedule.get(i); + } + return arr; + } + + /** + * Find the CFG output of a node + * @param node The node + * @return The CFG output of the node. + */ + private static CFGNode findSingleCFGOut(Node node) { + if (node instanceof StartNode) { + for(var n:node._outputs) + if (n instanceof FunNode fun && "main".equals(fun._name) ) + return fun; + return null; + } + if( node instanceof FunNode fun ) return fun.uctrl(); + assert node._outputs.stream().filter(x -> x instanceof CFGNode).limit(2).count()<=1; + for(var n:node._outputs) if(n instanceof CFGNode cfg) return cfg; + return null; + } + + /** + * Build the final data structure after all the scheduling happened. + * @param start The start node + * @return The final schedule. + */ + private Block build(Node start) { + var blocks = new IdentityHashMap<Node, Block>(); + var queue = new Stack<NodeData>(); + queue.push(d(start)); + + // Visit all CFG nodes and create blocks for them. + // This can combine blocks. + while(!queue.isEmpty()) { + var data = queue.pop(); + var first = data.node; + Node prev; + var last = first; + Node[] arr = new Node[0]; + if (first instanceof RegionNode) { + var nodes = new ArrayList<Node>(); + for(var out: first._outputs) { + if (out instanceof PhiNode) { + if(od(out).isPresent()) nodes.add(out); + } + } + arr = nodes.toArray(arr); + } + arr = appendNodes(arr, null, data.block); + assert !(first instanceof IfNode); + for(;;) { + prev = last; + last = findSingleCFGOut(last); + if (last == null) break; + if (last instanceof RegionNode) { + if (blocks.get(last) == null && last != first) + queue.push(d(last)); + break; + } + if (last instanceof ReturnNode) break; + if (last instanceof IfNode if_) { + for(var out:if_._outputs) if (out instanceof CFGNode && blocks.get(out) == null) queue.push(d(out)); + break; + } + data = d(last); + arr = appendNodes(arr, data.node, data.block); + } + blocks.put(first, switch(last) { + case null -> new Block(arr, null, -1, new Block[0]); + case IfNode i -> new Block(arr, i, -1, new Block[2]); + case RegionNode r -> new Block(arr, r, r._inputs.find(prev), new Block[1]); + case ReturnNode r -> new Block(arr, r , -1, new Block[0]); + default -> throw new AssertionError("Unexpected block exit node"); + }); + } + // Update the next pointer of all blocks. + for (var block: blocks.values()) { + switch(block.exit) { + case null: break; + case IfNode i: + for(var out:i._outputs) if (out instanceof CProjNode p) block.next[p._idx] = blocks.get(p); + break; + case RegionNode r: + block.next[0] = blocks.get(r); + break; + default: + assert block.exit instanceof ReturnNode; + } + } + // And return the block for the start node. + return blocks.get(start); + } + + /** + * Create a schedule for the program reachable from start. + * @param start The start node + * @return The final schedule. + */ + public static Scheduler.Block schedule(StartNode start) { + var scheduler = new Scheduler(); + scheduler.doMarkAlive(start); + scheduler.doBuildCTF(start); + scheduler.doSchedule(); + return scheduler.build(start); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java b/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java new file mode 100644 index 00000000..f7e0c4ad --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java @@ -0,0 +1,174 @@ +package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Eval2; +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.node.Node; +import java.util.ArrayList; +import java.util.Objects; +import java.util.Random; +import java.util.function.Consumer; + +/** + * Implementation of the fuzzer. + * Random testcases are generated using the ScriptGenerator. + * This will generate a script similar to how the parser parses it but instead of parsing it will + * randomly choose a variation the parser would parse and generate it. + * These scripts are the parsed by the parser and all exceptions are caught and filtered + * to only show one occurrence of one problem. This includes script which generate different + * results when the compiled graph with and without peeps is executed. + * To aid debugging scripts that cause errors are then reduced by applying rules and checking + * that the same issue persists. + */ +public class Fuzzer { + + private static final int EVAL_TIMEOUT = 1000; + + /** + * List of exceptions already encountered. + * This is used to filter new exceptions and discard them if they are already found. + */ + private final ArrayList<Throwable> exceptions = new ArrayList<>(); + + /** + * Filter and record an exception in a script. + * @param e The exception caused by the script + * @param script The script causing the exception + * @param reproducer Reproducer which should be used to reproduce the test case. + */ + private void recordException(Throwable e, String script, Consumer<String> reproducer, long seed) { + for (var ex : exceptions) { + if (FuzzerUtils.isExceptionFromSameCause(ex, e)) return; + } + exceptions.add(e); + System.out.println("========== Stack =========="); + e.printStackTrace(System.out); + System.out.println("========== Seed =========="); + System.out.println(seed); + //System.out.println("========== Code ==========="); + //System.out.println(script); + System.out.println("========= Reduced ========="); + System.out.println(Reducer.reduce(script, e, reproducer)); + System.out.println("==========================="); + System.out.flush(); + } + + private static boolean neq(Object a, Object b) { + if (Objects.equals(a, b)) return false; + return true; + } + + /** + * Check that two graphs result in the same output when supplied with a value + * @param e1 Evaluator for the first graph + * @param e2 Evaluator for the second graph + * @param in The input value to both graphs to test for an equal output + */ + private static void checkGraphs(CodeGen code1, CodeGen code2, long in) { + var r1 = Eval2.eval(code1, in, EVAL_TIMEOUT); + var r2 = Eval2.eval(code2, in, EVAL_TIMEOUT); + if( r1 == null || r2 == null ) return; // Timeout test invalid + if( neq(r1, r2) ) + throw new RuntimeException("Different calculations values " + r1 + " vs " + r2); + } + + /** + * Run checks for script. Compile the script with peeps enabled and disabled. + * Check that exceptions raised in the parser by both methods are the same and only happens if the script may be invalid. + * If the script was successfully parsed check that both version behave the same. + * @param script The script to test + * @param valid If the script is definitely valid. If not some exceptions may be suppressed. + */ + private static void runCheck(String script, boolean valid) { + CodeGen code1; + try { + code1 = FuzzerUtils.parse(script, 123); + } catch (RuntimeException e1) { + try { + FuzzerUtils.parse(script, 456); + } catch (RuntimeException e2) { + if (FuzzerUtils.isExceptionFromSameCause(e1, e2)) { + if (!valid || e1.getClass() == Parser.ParseException.class) return; + } else { + e1.addSuppressed(e2); + } + } + throw e1; + } + CodeGen code2 = FuzzerUtils.parse(script, 456); + checkGraphs(code1, code2, 0); + checkGraphs(code1, code2, 1); + checkGraphs(code1, code2, 10); + } + + /** + * Check that the script does not result in exceptions. + * @param script The script to test. + * @param valid If the script is valid or if it might contain syntax errors. + */ + private void check(String script, boolean valid, long seed) { + try { + runCheck(script, valid); + } catch (Throwable e) { + recordException(e, script, s->runCheck(s, valid), seed); + } + } + + /** + * Run one test with the given seed, and check for peephole counts. + * @param seed The seed to use for generating this test case + */ + public int fuzzPeepTiming(long seed, int max_nids) { + var rand = new Random(seed); + var sb = new StringBuilder(); + new ScriptGenerator(rand, sb, true).genProgram(); + try { + var code = new CodeGen(sb.toString()); + + code.parse(); + int nids = code.UID(); + if( nids <= max_nids ) return max_nids; // Smaller, looking for trends as we grow + int parse_peeps= code._iter_cnt; + int parse_nops = code._iter_nop_cnt; + double parse_nop_ratio = (double)parse_nops/parse_peeps; + double parse_peeps_per_node = (double)parse_peeps/nids; + + code.opto(); + int iter_peeps= code._iter_cnt; + int iter_nops = code._iter_nop_cnt; + double iter_nop_ratio = (double)iter_nops/iter_peeps; + double iter_peeps_per_node = (double)iter_peeps/nids; + + System.out.printf("%6d | Parsing: peeps: %5d, nops: %5d, ratio: %5.3f, last UID: %5d ps/node: %5.3f | Iter: peeps: %6d, nops: %6d, ratio: %5.3f, ps/node: %5.3f\n", + seed, + parse_peeps,parse_nops,parse_nop_ratio,nids,parse_peeps_per_node, + iter_peeps , iter_nops, iter_nop_ratio, iter_peeps_per_node); + return nids; + } catch (Throwable e) { + // ignore exceptions, let the normal fullPeeps catch and reduce, + // we're interested in valid program optimizations + } + return max_nids; + } + + + /** + * Run one test with the given seed. + * @param seed The seed to use for generating this test case + */ + public void fuzzPeeps(long seed) { + var rand = new Random(seed); + var sb = new StringBuilder(); + var valid = new ScriptGenerator(rand, sb, true).genProgram(); + check(sb.toString(), valid, seed); + } + + + /** + * Check that no exceptions happened. + * @return true if no exception happened + */ + public boolean noExceptions() { + return exceptions.isEmpty(); + } +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java new file mode 100644 index 00000000..0781ad67 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java @@ -0,0 +1,111 @@ +package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.CodeGen; +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StopNode; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.util.BitSet; + +/** + * Some utilities for the fuzzer. + */ +class FuzzerUtils { + + /** + * Get the val of a private field + */ + @SuppressWarnings("unchecked") + public static <T> T getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException { + return (T) (obj instanceof Class<?> clz ? getField(clz, name).get(null) : getField(obj.getClass(), name).get(obj)); + } + + /** + * Get access to a private field + */ + public static Field getField(Class<?> clz, String name) throws NoSuchFieldException { + var field = clz.getDeclaredField(name); + try { + field.setAccessible(true); + } catch (Throwable e) { + // Intentionally left empty + } + return field; + } + + /** + * Try to rethrow an exception or create a runtime exception and throw it. + */ + public static RuntimeException rethrow(Throwable e) { + if (e instanceof RuntimeException re) return re; + if (e instanceof Error er) throw er; + return new RuntimeException(e); + } + + /** + * Some problems might trigger debug output. Suppress this output with a null print stream during fuzzing. + */ + public static final PrintStream NULL_PRINT_STREAM = new PrintStream(OutputStream.nullOutputStream()); + + /** + * Check that the messages are similar. + */ + private static boolean similarMessages(String a, String b) { + if (a == null || b == null) return true; + a = a.replaceAll("'\\w+'", "'normalized'").replaceAll("variable \\w+", "variable normalized"); + b = b.replaceAll("'\\w+'", "'normalized'").replaceAll("variable \\w+", "variable normalized"); + return a.equals(b); + } + + /** + * Check if both stack traces have the same first entry. Empty stack traces can happen with NPE optimization + * and allow the other to be any stack trace. + */ + private static boolean sameStackTraceOrigin(StackTraceElement[] a, StackTraceElement[] b) { + if (a == null || a.length == 0 || b == null || b.length == 0) return true; // One or both stack traces are missing. + return a[0].equals(b[0]); + } + + /** + * Checks if two exceptions are caused by the same problem. + */ + public static boolean isExceptionFromSameCause(Throwable a, Throwable b) { + if (a == null || b == null) return a == b; // Handle the null case + if (a.getClass() != b.getClass()) return false; // Different classes + if (!similarMessages(a.getMessage(), b.getMessage())) return false; // Different messages. Null is allowed as wildcard + var s1 = a.getStackTrace(); + var s2 = b.getStackTrace(); + if (!sameStackTraceOrigin(s1, s2)) return false; // Different stack trace origins + if (!isExceptionFromSameCause(a.getCause(), b.getCause())) return false; // Different causes + var o1 = a.getSuppressed(); + var o2 = b.getSuppressed(); + if (o1 == null || o2 == null || o1.length != o2.length) return o1 == o2; // Different suppression + for (int i=0; i<o1.length; i++) { + if (!isExceptionFromSameCause(o1[i], o2[i])) return false; // Different suppression + } + return true; + } + + /** + * Parse script with peepholes enabled or disabled + */ + public static CodeGen parse(String script, long workListSeed) { + var err = System.err; + var out = System.out; + try { + System.setErr(NULL_PRINT_STREAM); + System.setOut(NULL_PRINT_STREAM); + var code = new CodeGen(script, TypeInteger.BOT, workListSeed); + return code.parse().opto().typeCheck().GCM().localSched(); + } finally { + System.setErr(err); + System.setOut(out); + } + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java b/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java new file mode 100644 index 00000000..3731b40c --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java @@ -0,0 +1,176 @@ +package com.seaofnodes.simple.fuzzer; + +import java.util.Arrays; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class is used to reduce test cases created by the fuzzer. + * It applies rewrite rules to the test case and checks that the + * test still produces the same exception. + * This is done in a fixpoint iteration until every rewrite produces + * no or a different exception. + */ +public class Reducer { + + private interface Replacer extends BiConsumer<StringBuilder, Matcher> {} + + /** + * Holder for the rewrite rules + */ + private record Replace(Pattern pattern, boolean iterate, Replacer... replacements) { + Replace(Pattern p, Object... replacements) { + this(p, replacements[0] == Boolean.TRUE, Arrays.stream(replacements).filter(o->!(o instanceof Boolean)).map(Replace::from).toArray(Replacer[]::new)); + } + private static Replacer from(Object o) { + if (o instanceof Replacer r) return r; + if (o instanceof Integer i) return (s, m)->s.append(m.group(i)); + if (o instanceof String r) return (s, m)->s.append(r); + throw new IllegalArgumentException(); + } + } + + /** + * List of rewrite rules. + */ + private static final Replace[] REPLACEMENTS = { + new Replace(Pattern.compile("\\b(?:[^\\W0]|\\w\\w+)\\b"), "0", "1"), + new Replace(Pattern.compile("(?<=\\n|^)[^\\n]+(?=\\n|$)"), true, ""), + new Replace(Pattern.compile("(?<=[+\\-*/<>=(])--"), ""), + new Replace(Pattern.compile("\\b--"), "+"), + new Replace(Pattern.compile("-"), ""), + new Replace(Pattern.compile("\\b\\+-"), "-"), + new Replace(Pattern.compile("(?<![+\\-*/<>=])(?:-|\\*|/|>=|<=|>|<|!=|==)(?![+\\-*/<>=])"), "+"), + new Replace(Pattern.compile("\\{((?:[^{}]|\\{})+)(?:\\n[ \\t]*)?}"), "", "{}", 1), + new Replace(Pattern.compile("(?<=\\n|^)([ \\t]*)\\{\\n(.*?)\\n\\1}", Pattern.DOTALL), 2), + new Replace(Pattern.compile("\\(([^()]+)\\)"), 1), + new Replace(Pattern.compile("\\d+\\+"), ""), + new Replace(Pattern.compile("\\+\\d+"), ""), + new Replace(Pattern.compile("\\n\\n+"), "\n"), + new Replace(Pattern.compile("^\\n+|\\n+$"), ""), + new Replace(Pattern.compile("else *"), ""), + new Replace(Pattern.compile("\\bif\\([^()]+\\) *"), ""), + new Replace(Pattern.compile("\\bwhile\\([^()]+\\) *"), ""), + new Replace(Pattern.compile("\\bstruct \\w+ \\{[^{}]*}"), "") + }; + + /** + * Predicate to test if the reduced script generates the same error. + */ + private final Predicate<String> tester; + + /** + * Create a reducer with a test function. + * @param tester Test function which returns true when the same issue is found in the script. + */ + private Reducer(Predicate<String> tester) { + this.tester = tester; + } + + /** + * Rewrites the script with one rewrite rule. + */ + private String doReplacement(String script, Pattern pattern, Replacer[] replacements) { + var matcher = pattern.matcher(script); + var sb = new StringBuilder(); + var last = 0; + var skip = 0; + outer: + while (matcher.find(last + skip)) { + sb.append(script, last, matcher.start()); + last = matcher.end(); + skip = last == matcher.start() ? 1 : 0; + var pos = sb.length(); + for (var replacement : replacements) { + replacement.accept(sb, matcher); + var tail = sb.length(); + sb.append(script, last, script.length()); + if (tester.test(sb.toString())) { + sb.setLength(tail); + continue outer; + } + sb.setLength(pos); + } + last = matcher.start(); + skip = 1; + } + sb.append(script, last, script.length()); + return sb.toString(); + } + + /** + * Clean up variable names from the random ones generated by the fuzzer. + */ + private String cleanVariables(String script) { + var matcher = Pattern.compile("\\b([0-9a-zA-Z_]+) ([0-9a-zA-Z_]+)").matcher(script); + int[] num = {0, 0}; + while (matcher.find()) { + var type = matcher.group(1); + var var = matcher.group(2); + if (type.equals("new")) continue; + String prefix = "v"; + int t = 0; + if (type.equals("struct")) { + t = 1; + prefix = "s"; + } + if (var.matches(prefix+"\\d+")) continue; + while (script.contains(prefix+num[t])) num[t]++; + var n = script.replaceAll("\\b"+var+"\\b", prefix + num[t]); + if (tester.test(n)) { + script = n; + } + } + return script; + } + + /** + * Run all rewrite rules once. + */ + private String doAllReplacements(String script) { + for (var replace: REPLACEMENTS) { + String old; + do { + old = script; + script = doReplacement(script, replace.pattern, replace.replacements); + } while (replace.iterate && !old.equals(script)); + } + return script; + } + + /** + * Reduce the script to a minimal version which produces the same problem with a test function. + * @param tester Test function which returns true when the same issue is found in the script. + */ + public static String reduce(String script, Predicate<String> tester) { + var reducer = new Reducer(tester); + String old; + do { + old = script; + script = reducer.doAllReplacements(script); + script = reducer.cleanVariables(script); + } while (!script.equals(old)); + return script; + } + + /** + * Reduce the script to a minimal version which produces the same problem with a reproducer. + * @param ex The exception originally caused by the script + * @param reproducer Test function which throws an exception which will be checked against ex. + */ + public static String reduce(String script, Throwable ex, Consumer<String> reproducer) { + return reduce(script, s->{ + Throwable nex = null; + try { + reproducer.accept(s); + } catch (Throwable e) { + nex = e; + } + return FuzzerUtils.isExceptionFromSameCause(nex, ex); + }); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java b/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java new file mode 100644 index 00000000..f119fa1a --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java @@ -0,0 +1,869 @@ +package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.ScopeNode; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.function.Predicate; +import static com.seaofnodes.simple.Parser.KEYWORDS; + + +/** + * Generate a pseudo random script. + * This generator will generate the same script for two random number generators initialized with the same seed. + * To generate valid code it implements the parser but instead of parsing it emits the statements and expressions. + * It is guaranteed to terminate. + */ +public class ScriptGenerator { + + private static final int MAX_STATEMENTS_PER_BLOCK = 5; + private static final int MAX_BINARY_EXPRESSIONS_PER_EXPRESSION = 10; + private static final int MAX_UNARY_EXPRESSIONS_PER_EXPRESSION = 6; + private static final int MAX_NAME_LENGTH = 10; + private static final int MAX_BLOCK_DEPTH = 4; + private static final int MAX_EXPRESSION_DEPTH = 10; + private static final int MAX_SUFFIX_RECURSIVE_DEPTH = 10; + + /** + * Number of spaces per indentation + */ + private static final int INDENTATION = 4; + /** + * Valid characters for identifiers. The last 10 are not valid for the first character. + */ + private static final String VAR_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; + + /** + * Flag for a statement that it does not pass on control flow to the next statement. + */ + private static final int FLAG_STOP = 0x01; + /** + * Flag for a statement that it contains a final if statement without an else branch. + */ + private static final int FLAG_IF_WITHOUT_ELSE = 0x02; + /** + * Binary operators. + */ + private static final String[] BINARY_OP = {"==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/", "&", ">>", ">>>", "<<"}; + /** + * Unary operators. + */ + private static final String[] UNARY_OP = {"-"}; + + private static class Type { + final String name; + Type(String name) {this.name=name;} + boolean isa(Type other) { return this == other; } + } + + private static class TypeStruct extends Type { + record Field (String name, Type type, TypeStruct struct) {} + Field[] fields; + final TypeNullable nullable = new TypeNullable(this); + TypeStruct(String name) { + super(name); + } + boolean isa(Type other) { return this == other || other == nullable; } + } + + private static class TypeNullable extends Type { + + final Type base; + + TypeNullable(Type base) { + super(base.name+"?"); + this.base = base; + } + + } + + private static final Type TYPE_FLT = new Type("flt"); + + private static class TypeInt extends Type { + + TypeInt(String name) { + super(name); + } + + @Override + boolean isa(Type other) { + return super.isa(other) || other == TYPE_FLT || other instanceof TypeInt; + } + } + + private static final List<TypeInt> INTTYPES = new ArrayList<>(); + + static { + for( var e : Parser.defaultTypes().entrySet() ) { + if( e.getValue() instanceof TypeInteger ) { + INTTYPES.add(new TypeInt(e.getKey())); + } + } + } + + private static final Type TYPE_INT = INTTYPES.stream().filter(t->t.name.equals("int")).findAny().get(); + private static final Type TYPE_BOOL = INTTYPES.stream().filter(t->t.name.equals("bool")).findAny().get(); + + private static class Variable { + final String name; + final Type declared; + final Variable shadowing; + Type type; + boolean shadowed = false; + + Variable(String name, Type type, Variable var) { + this.name = name; + this.declared = type; + this.type = type; + this.shadowing = var; + } + } + + /** + * The random number generator used for random decisions. + */ + private final Random random; + /** + * Indentation depth. + */ + private int indentation = 0; + /** + * Output string builder + */ + private final StringBuilder sb; + /** + * Number of nested loops + */ + private int loopDepth = 0; + /** + * Current scope start in the variables list. + */ + private int currScopeStart = 0; + /** + * The depth of blocks allowed. + */ + private int depth = MAX_BLOCK_DEPTH; + /** + * The depth of expressions allowed. + */ + private int exprDepth = MAX_EXPRESSION_DEPTH; + /** + * Current variables in scope. + */ + private final ArrayList<Variable> variables = new ArrayList<>(); + /** + * All defined structs. + */ + private final ArrayList<TypeStruct> structs = new ArrayList<>(); + /** + * All forward declared structs + */ + private final ArrayList<TypeStruct> forwardStructs = new ArrayList<>(); + /** + * Allow to declare structs. + */ + private boolean allowStructs = true; + /** + * If this needs to generate valid scripts or is also allowed to generate invalid ones. + */ + private boolean generateValid; + /** + * If this script might be invalid. + */ + private boolean maybeInvalid = false; + + + /** + * + * @param random the random number generator. + * @param sb the script will be written to this string builder. + * @param generateValid if the script should be guaranteed valid or if it is allowed to contain bugs. + */ + public ScriptGenerator(Random random, StringBuilder sb, boolean generateValid) { + this.random = random; + this.sb = sb; + this.generateValid = generateValid; + } + + /** + * Random number with log distribution + * @param max The maximum value + * @return A random number in the range [0, max) with higher numbers being more uncommon. + */ + private int randLog(int max) { + max--; + int n = 1<<max; + var num = random.nextInt(n); + for (int i=max-1; i>=0; i--) { + if ((num & (1<<i))!=0) return max-i-1; + } + return max; + } + + /** + * Helper function to print indentation. + * @return sb for chaining + */ + private StringBuilder printIndentation() { + return sb.repeat(' ', indentation); + } + + /** + * Generate a random name for variables. This might be invalid when it is a keyword. + * @return The random name generated + */ + private StringBuilder getRandomName() { + int len = random.nextInt(MAX_NAME_LENGTH) + 1; + StringBuilder sb = new StringBuilder(len); + sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()-10))); + for (int i=1; i<len; i++) + sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()))); + return sb; + } + + /** + * Should invalid code be generated? + * @return True if invalid code should be generated + */ + private boolean generateInvalid() { + if (generateValid || random.nextInt(1000)>2) return false; + maybeInvalid = true; + return true; + } + + /** + * Lookup a variable by name + * @param name name of the variable + * @return The index of the variable in variables or -1 + */ + private int lookup(String name) { + for (int i=variables.size()-1; i>=0; i--) { + if (variables.get(i).name.equals(name)) return i; + } + return -1; + } + + /** + * Lookup a variable by name + * @param name name of the variable + * @return THe variable of null if not found + */ + private Variable lookupVar(String name) { + int idx = lookup(name); + return idx == -1 ? null : variables.get(idx); + } + + /** + * Add a variable to the current scope + * @param name The name of the variable + * @param type The declared type of the variable + */ + private void addVariable(String name, Type type) { + var old = lookupVar(name); + variables.add(new Variable(name, type, old)); + if (old != null) old.shadowed = true; + } + + /** + * Pop variables to level to + * @param to Level until which variables should be popped + */ + private void popVariables(int to) { + while (variables.size() > to) { + var v = variables.removeLast(); + if (v.shadowing != null) v.shadowing.shadowed = false; + } + } + + /** + * Get a random visible variable patching pred + * @param pred The predicate the variable needs to match + * @return A random visible variable matching pred or null if no variable mached pred. + */ + private Variable findVisibleVariablesMatching(Predicate<Variable> pred) { + int num=0; + for (var v:variables) { + if (!v.shadowed && pred.test(v)) num++; + } + if (num == 0) return null; + num = random.nextInt(num); + for (var v:variables) { + if (!v.shadowed && pred.test(v) && num-- == 0) return v; + } + throw new AssertionError(); + } + + /** + * Get a structure by name + * @param name Name of the structure + * @return The structure of null + */ + private TypeStruct getStruct(String name) { + for (var s:structs) { + if (s.name.equals(name)) return s; + } + for (var s:forwardStructs) if (s.name.equals(name)) return s; + return null; + } + + /** + * Generate a variable name. This might be new random name or a name from an outer scope. + * If the program is allowed to be invalid this can return keywords and already used variable names in the current scope. + * @return The variable name. + */ + private String getVarName() { + if (currScopeStart > 0 && random.nextInt(10) > 7) { + // Use a variable outside the current scope. + var v = variables.get(random.nextInt(currScopeStart)); + if (lookup(v.name) < currScopeStart) return v.name; + if (!generateValid) { + maybeInvalid = true; + return v.name; + } + } + // Generate a new random variable name + StringBuilder sb = getRandomName(); + var v = sb.toString(); + if (!generateValid && (KEYWORDS.contains(v) || lookup(v) >= currScopeStart)) { + maybeInvalid = true; + return v; + } + while (KEYWORDS.contains(v) || lookup(v) >= currScopeStart) { + sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()))); + v = sb.toString(); + } + return v; + } + + private <T> T randFromList(List<T> list) { + return list.get(random.nextInt(list.size())); + } + + /** + * Get a random structure name + * @return A random structure name + */ + private String getStructName() { + if (!structs.isEmpty() && generateInvalid()) return randFromList(structs).name; + if (!forwardStructs.isEmpty() && generateInvalid()) return randFromList(forwardStructs).name; + + // Generate a new random struct name + StringBuilder sb = getRandomName(); + var v = sb.toString(); + if (!generateValid && (KEYWORDS.contains(v) || getStruct(v) != null)) { + maybeInvalid = true; + return v; + } + while (KEYWORDS.contains(v) || getStruct(v) != null) { + sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()))); + v = sb.toString(); + } + return v; + } + + public TypeInt getIntType() { + return randFromList(INTTYPES); + } + + /** + * Get a random type + * @return A random type + */ + private Type getType(boolean allowForward) { + int t = random.nextInt(10); + if( t<5 || structs.isEmpty() ) return getIntType(); + if( t<7 ) return TYPE_FLT; + if (allowForward && random.nextInt(10)==0) { + int i=random.nextInt(forwardStructs.size()+1); + if (i>0) return forwardStructs.get(i-1).nullable; + var name = getStructName(); + var struct = new TypeStruct(name); + forwardStructs.add(struct); + return struct.nullable; + } + TypeStruct struct = randFromList(structs); + if (random.nextBoolean()) return struct.nullable; + return struct; + } + + private Type getType() { + return getType(false); + } + + /** + * Generates a program and writes it into the string builder supplied in the constructor. + * @return If the program is allowed to be invalid returns if something potentially invalid was generated. + */ + public boolean genProgram() { + var oldGenerateValid = generateValid; + if (!generateValid && random.nextBoolean()) generateValid = true; + this.maybeInvalid = false; + addVariable(ScopeNode.ARG0, TYPE_INT); + currScopeStart = variables.size(); + if ((genStatements() & FLAG_STOP) == 0) { + if (generateValid || random.nextInt(10)<7) { + genReturn(); + } else { + this.maybeInvalid = true; + } + } + generateValid = oldGenerateValid; + return !this.maybeInvalid; + } + + /** + * Generate a list of statements. + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the last statement generated + */ + public int genStatements() { + var num = random.nextInt(MAX_STATEMENTS_PER_BLOCK * (depth+1)); + for (int i=0; i<num; i++) { + printIndentation(); + var stop = genStatement(); + sb.append("\n"); + if ((stop & FLAG_STOP) != 0) return stop; + } + return 0; + } + + /** + * Generate a single statement + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement + */ + public int genStatement() { + return switch (random.nextInt(12)) { + case 0 -> allowStructs || generateInvalid() ? genStruct() : genAssignment(); + case 1 -> genBlock(); + case 2 -> genIf(); + case 3 -> genCountedLoop(); + case 4 -> genWhile(); + case 5, 6 -> genDecl(); + case 7, 8, 9 -> genAssignment(); + case 10 -> genNullCheck(); + default -> genExit(); + }; + } + + /** + * Generate a new structure + * @return 0 + */ + public int genStruct() { + int idx = random.nextInt(forwardStructs.size()+10); + TypeStruct struct; + if (idx < 10) { + var name = getStructName(); + struct = new TypeStruct(name); + forwardStructs.add(struct); + } else { + struct = forwardStructs.get(idx-10); + } + var fields = new TypeStruct.Field[generateInvalid() ? 0 : random.nextInt(10)]; + sb.append("struct ").append(struct.name).append(" {\n"); + indentation += INDENTATION; + for (int i=0; i<fields.length; i++) { + var fieldName = getRandomName(); + var type = getType(true); + printIndentation().append(type.name).append(" ").append(fieldName).append(";\n"); + fields[i] = new TypeStruct.Field(fieldName.toString(), type, struct); + } + indentation -= INDENTATION; + printIndentation().append("};"); + struct.fields = fields; + forwardStructs.remove(struct); + structs.add(struct); + return 0; + } + + /** + * Generate a statement for if, else and while blocks. + * This is special since it disallows declarations and prefers to generate blocks. + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement + */ + public int genStatementBlock() { + var oldAllowStructs = allowStructs; + allowStructs = false; + indentation += INDENTATION; + int res; + if (depth == 0) { + res = random.nextBoolean() ? genAssignment() : genExit(); + } else { + depth--; + res = switch (random.nextInt(12)) { + case 1, 2, 3, 4, 5 -> { + depth++; + indentation -= INDENTATION; + var ret = genBlock(); + indentation += INDENTATION; + depth--; + yield ret; + } + case 6 -> genIf(); + case 7 -> genCountedLoop(); + case 8 -> genWhile(); + case 9 -> genAssignment(); + case 10 -> genNullCheck(); + default -> genExit(); + }; + depth++; + } + indentation -= INDENTATION; + allowStructs = oldAllowStructs; + return res; + } + + /** + * Generate an exit. This can be return or break and continue when in a loop. + * @return FLAG_STOP + */ + public int genExit() { + if (loopDepth == 0 && generateValid) return genReturn(); + return switch (random.nextInt(7)) { + case 0,1,2 -> { + if (loopDepth == 0) maybeInvalid = true; + sb.append("continue;"); + yield FLAG_STOP; + } + case 3,4,5 -> { + if (loopDepth == 0) maybeInvalid = true; + sb.append("break;"); + yield FLAG_STOP; + } + default -> genReturn(); + }; + } + + /** + * Generate a block statement. After a certain depth only empty blocks are generated to ensure termination of the generator. + * @return flag FLAG_STOP for the generated statement + */ + public int genBlock() { + if (depth == 0) { + sb.append("{}"); + return 0; + } + var oldAllowStructs = allowStructs; + var oldCSS = currScopeStart; + allowStructs = false; + currScopeStart = variables.size(); + depth--; + sb.append("{\n"); + indentation += INDENTATION; + var stop = genStatements(); + indentation -= INDENTATION; + printIndentation().append("}"); + popVariables(currScopeStart); + depth++; + currScopeStart = oldCSS; + allowStructs = oldAllowStructs; + return stop & ~FLAG_IF_WITHOUT_ELSE; + } + + /** + * Generate an if statement with an optional else block. + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement + */ + public int genIf() { + sb.append("if("); + genExpression(TYPE_BOOL, true); + sb.append(") "); + var stop = genStatementBlock(); + if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) { + sb.append("\n"); + printIndentation().append("else "); + stop &= genStatementBlock(); + } else { + stop = FLAG_IF_WITHOUT_ELSE; + } + return stop; + } + + /** + * Generate a block with a null check + * if (nullable) { handle_nullable_as_non_nullable; } + */ + public int genNullCheck() { + var v = findVisibleVariablesMatching(va->va.declared instanceof TypeNullable); + if (v == null) return genIf(); + var n = (TypeNullable)v.declared; + boolean negate = random.nextBoolean(); + sb.append("if("); + if (negate) sb.append("!"); + sb.append(v.name).append(")"); + if (!negate) v.type = n.base; + var stop = genStatementBlock(); + v.type = n; + if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) { + sb.append("\n"); + printIndentation().append("else "); + if (negate) v.type = n.base; + stop &= genStatementBlock(); + v.type = n; + } else { + stop = FLAG_IF_WITHOUT_ELSE; + } + return stop; + } + + /** + * Generate a while loop. + * @return 0 + */ + public int genWhile() { + sb.append("while("); + genExpression(TYPE_BOOL, true); + sb.append(") "); + loopDepth++; + genStatementBlock(); + loopDepth--; + return 0; + } + + /** + * Generate a counted loop of the form + * <code> + * { + * int varname = start_expr; + * while (varname < end_expr) loop_body; + * } + * </code> + * @return 0 + */ + public int genCountedLoop() { + if (depth == 0) return genWhile(); + var oldCSS = currScopeStart; + currScopeStart = variables.size(); + sb.append("{\n"); + indentation += INDENTATION; + String name = getVarName(); + printIndentation().append("int ").append(name).append("="); + genExpression(TYPE_INT, true); + sb.append(";\n"); + addVariable(name, TYPE_INT); + printIndentation().append("while(").append(name).append("<"); + genExpression(TYPE_INT, true); + sb.append(") {\n"); + indentation += INDENTATION; + depth--; + loopDepth++; + printIndentation().append(name).append("=").append(name).append("+"); + genExpression(TYPE_INT, true); + sb.append(";\n"); + genStatements(); + loopDepth--; + depth++; + indentation -= INDENTATION; + printIndentation().append("}\n"); + indentation -= INDENTATION; + printIndentation().append("}"); + popVariables(currScopeStart); + currScopeStart = oldCSS; + return 0; + } + + /** + * Generate a declaration statement. + * @return 0 + */ + public int genDecl() { + var type = getType(); + var name = getVarName(); + // Always make them mutable + sb.append(generateInvalid() ? getRandomName() : type.name).append(" !").append(name); + if (!(type instanceof TypeNullable) || random.nextBoolean()) { + sb.append("="); + genExpression(type, true); + } + sb.append(";"); + addVariable(name, type); + return 0; + } + + /** + * Generate an assignment statement. + * @return 0 + */ + public int genAssignment() { + Type type; + Type declared; + if (generateInvalid()) { + sb.append(getRandomName()); + type = getType(); + declared = type; + } else { + var v = findVisibleVariablesMatching(va->true); + if (v == null) return genDecl(); + sb.append(v.name); + type = v.type; + declared = v.declared; + } + if (generateInvalid()) { + var name = getRandomName(); + sb.append(".").append(name); + declared = getType(); + } else if (type instanceof TypeStruct s && s.fields.length > 0 && random.nextBoolean()) { + var field = s.fields[random.nextInt(s.fields.length)]; + sb.append(".").append(field.name); + declared = field.type; + } + sb.append("="); + genExpression(declared, true); + sb.append(";"); + return 0; + } + + /** + * Generate a return statement. + * @return FLAG_STOP + */ + public int genReturn() { + var type = getType(); + sb.append("return "); + genExpression(type, true); + sb.append(";"); + return FLAG_STOP; + } + + /** + * Generate a binary expression. + * This method does not care about operator precedence. + */ + public void genExpression(Type type, boolean change) { + if (change && generateInvalid()) type = getType(); + if (!(type instanceof TypeInt) && type != TYPE_FLT) { + genUnary(type, false); + return; + } + var num = randLog(MAX_BINARY_EXPRESSIONS_PER_EXPRESSION); + while(num-->0) { + genUnary(type, false); + sb.append(BINARY_OP[random.nextInt(BINARY_OP.length)]); + } + genUnary(type, false); + } + + /** + * Generate a unary expression. + */ + public void genUnary(Type type, boolean change) { + if (change && generateInvalid()) type = getType(); + if (!(type instanceof TypeInt) && type != TYPE_FLT) { + genSuffix(type, false); + return; + } + var num = randLog(MAX_UNARY_EXPRESSIONS_PER_EXPRESSION); + while(num-->0) { + sb.append(UNARY_OP[random.nextInt(UNARY_OP.length)]); + } + genSuffix(type, false); + } + + public TypeStruct.Field getField(Type type) { + int n=0; + for (var s: structs) { + for (var f: s.fields) { + if (f.type.isa(type)) n++; + } + } + if (n==0) return null; + n = random.nextInt(n); + for (var s: structs) { + for (var f: s.fields) { + if (f.type.isa(type)) if (n--==0) return f; + } + } + throw new AssertionError(); + } + + /** + * Generate + */ + public void genSuffix(Type type, boolean change) { + genSuffixRecursive(type, change, MAX_SUFFIX_RECURSIVE_DEPTH); + } + + public void genSuffixRecursive(Type type, boolean change, int depth) { + if (change && generateInvalid()) type = getType(); + if (depth > 0 && random.nextBoolean()) { + if (generateInvalid()) { + var field = getRandomName(); + var t = getType(); + genSuffixRecursive(t, true, depth-1); + sb.append(".").append(field); + return; + } else { + var field = getField(type); + if (field != null) { + genSuffixRecursive(field.struct, true, depth-1); + sb.append(".").append(field.name); + return; + } + } + } + genPrimary(type, false); + } + + /** + * Generate a primary expression. + */ + public void genPrimary(Type type, boolean change) { + if (change && generateInvalid()) type = getType(); + var rand = random.nextInt(10); + if (rand == 0 && exprDepth != 0) { + sb.append("("); + exprDepth--; + genExpression(type, false); + exprDepth++; + sb.append(")"); + } else if (rand < 6) { + genVariable(type); + } else { + genConst(type); + } + } + + /** + * Generate a constant. + */ + public void genConst(Type type) { + if (generateInvalid()) type = getType(); + if (type instanceof TypeInt || (type == TYPE_FLT && random.nextBoolean())) { + var rand = random.nextInt(10); + switch (rand) { + case 0 -> sb.append("true"); + case 1 -> sb.append("false"); + default -> sb.append(random.nextInt(1<<(rand-2))); + } + } else if (type == TYPE_FLT) { + sb.append(random.nextFloat()); + } else if (type instanceof TypeNullable n) { + if ((n.base instanceof TypeStruct s && s.fields == null) || random.nextBoolean()) { + sb.append("null"); + } else { + sb.append("new ").append(generateInvalid() ? getRandomName() : n.base.name); + } + } else { + sb.append("new ").append(generateInvalid() ? getRandomName() : type.name); + } + } + + /** + * Generate a variable. If there are none generate a number instead. + * If the program is allowed to be invalid a random name can be generated in some cases. + */ + public void genVariable(Type type) { + if (generateInvalid()) { + sb.append(getRandomName()); + return; + } + if (generateInvalid()) type = getType(); + var t = type; + var v = findVisibleVariablesMatching(va->va.type.isa(t)); + if (v == null) genConst(type); + else sb.append(v.name); + } + +} diff --git a/chapter20/src/test/java/com/seaofnodes/simple/progs/fcn.smp b/chapter20/src/test/java/com/seaofnodes/simple/progs/fcn.smp new file mode 100644 index 00000000..188e0a9f --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/progs/fcn.smp @@ -0,0 +1,15 @@ + +struct Person { + int age; +}; + +val fcn = { Person?[] ps, int x -> + val tmp = ps[x]; + if( ps[x] ) + ps[x].age++; +}; + +Person?[] !ps = new Person?[2]; +ps[0] = new Person; +ps[1] = new Person; +fcn(ps,1); diff --git a/chapter20/src/test/java/com/seaofnodes/simple/progs/fcn2.smp b/chapter20/src/test/java/com/seaofnodes/simple/progs/fcn2.smp new file mode 100644 index 00000000..049132d9 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/progs/fcn2.smp @@ -0,0 +1,5 @@ + +val fcn = (arg&1) + ? { int x -> x*x; } + : { int x -> x+x; }; +fcn(arg); diff --git a/chapter20/src/test/java/com/seaofnodes/simple/progs/fcn3.smp b/chapter20/src/test/java/com/seaofnodes/simple/progs/fcn3.smp new file mode 100644 index 00000000..a64564f0 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/progs/fcn3.smp @@ -0,0 +1,42 @@ + +//val fact = { int n -> n <= 1 ? 1 : fact(n-1)*n; } +struct LL { LL? next; int payload; }; + + +struct String { + + val _hashCodeString = { -> + int hash=0; + for( int i=0; i< cs#; i++ ) + hash = hash*31 + cs[i]; + if( !hash ) hash = 123456789; + return hash; + }; + + val equals = { String s -> + if( self == s ) return true; + if( cs# != s.cs# ) return false; + for( int i=0; i< #; i++ ) + if( [i] != s.cs[i] ) + return false; + return true; + }; + + int _hashCode = hash(); + u8[] cs; +}; + + + +val hashCode= { String self -> + int hash = self._hashCode + ? self._hashCode + : self._hashCode = _hashCodeString(self); + return hash; +}; + + +String s = new String { cs = new u8[17]; }; +s.cs[0] = 67; // C +s.cs[1] = 108; // l +hashCode(s); diff --git a/chapter20/src/test/java/com/seaofnodes/simple/progs/sieve.smp b/chapter20/src/test/java/com/seaofnodes/simple/progs/sieve.smp new file mode 100644 index 00000000..0788e6c5 --- /dev/null +++ b/chapter20/src/test/java/com/seaofnodes/simple/progs/sieve.smp @@ -0,0 +1,32 @@ + +int[] !ary = new int[arg], !primes = new int[arg]; +int nprimes = 0, p=2; +// Find primes while p^2 < arg +while( p*p < arg ) { + // skip marked non-primes + while( ary[p]==1 ) p = p + 1; + // p is now a prime + primes[nprimes] = p; nprimes = nprimes + 1; + // Mark out the rest non-primes + int i = p + p; + while( i < ary# ) { + ary[i] = 1; + i = i + p; + } + p = p + 1; +} +// Now just collect the remaining primes, no more marking +while( p < arg ) { + if( ary[p] == 0 ) { + primes[nprimes] = p; nprimes = nprimes + 1; + } + p = p + 1; +} +// Copy/shrink the result array +int[] !rez = new int[nprimes]; +int j = 0; +while( j < nprimes ) { + rez[j] = primes[j]; + j = j + 1; +} +return rez; |
