summaryrefslogtreecommitdiff
path: root/print/src
diff options
context:
space:
mode:
Diffstat (limited to 'print/src')
-rw-r--r--print/src/main/java/com/seaofnodes/print/ASMPrinter.java157
-rw-r--r--print/src/main/java/com/seaofnodes/print/AssemblyAdapter.java26
-rw-r--r--print/src/main/java/com/seaofnodes/print/BaseNode.java37
-rw-r--r--print/src/main/java/com/seaofnodes/print/ExprPrinter.java56
-rw-r--r--print/src/main/java/com/seaofnodes/print/IRAdapter.java34
-rw-r--r--print/src/main/java/com/seaofnodes/print/IRPrinter.java293
-rw-r--r--print/src/test/java/com/seaofnodes/print/PrinterTest.java153
7 files changed, 756 insertions, 0 deletions
diff --git a/print/src/main/java/com/seaofnodes/print/ASMPrinter.java b/print/src/main/java/com/seaofnodes/print/ASMPrinter.java
new file mode 100644
index 00000000..89c64b2c
--- /dev/null
+++ b/print/src/main/java/com/seaofnodes/print/ASMPrinter.java
@@ -0,0 +1,157 @@
+package com.seaofnodes.print;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.IdentityHashMap;
+import static com.seaofnodes.print.IRAdapter.Kind;
+
+/** Assembly listing layout shared by every chapter with instruction selection. */
+public final class ASMPrinter<N extends BaseNode<N>> {
+ private final AssemblyAdapter<N> _a;
+ private final IRAdapter<N> _ir;
+ private final StringBuilder _text=new StringBuilder();
+ private final IdentityHashMap<N,Boolean> _seen=new IdentityHashMap<>();
+ private int _address;
+ public ASMPrinter(AssemblyAdapter<N> adapter) { _a=adapter; _ir=adapter.ir; }
+
+ public String print() {
+ _text.setLength(0);
+ _seen.clear();
+ _address=0;
+ ArrayList<N> blocks=_a.blocks();
+ if( blocks.isEmpty() ) return "No scheduled code";
+ for( int i=0; i<blocks.size(); i++ ) {
+ N fun=blocks.get(i);
+ if( _ir.kind(fun)!=Kind.FUN ) continue;
+ _text.append("\n--- ").append(_ir.signature(fun)).append("---------------------------\n");
+ if( _a.encoded() ) _address=(_address+15)&-16;
+ if( _a.prologue(fun) ) instruction(fun,blocks,i);
+ for( ; i<blocks.size() && _ir.kind(blocks.get(i))!=Kind.RETURN; i++ )
+ block(blocks.get(i),blocks,i);
+ _text.append("--- ").append(_ir.functionName(fun)).append("---------------------------\n");
+ }
+ for( Pool pool : _a.pools(_address) ) _text.append(pool(pool));
+ return _text.toString();
+ }
+
+ private String label(N n) { return (_ir.kind(n)==Kind.LOOP ? "LOOP" : "L")+_ir.id(n); }
+ private void block(N bb, ArrayList<N> blocks, int idx) {
+ if( _a.labelBlock(bb) ) _text.append(label(bb)).append(":\n");
+ if( _ir.kind(bb)==Kind.CALL ) return;
+ boolean packed=false;
+ for( int i=0; i<_ir.nOuts(bb); i++ ) {
+ N n=_ir.out(bb,i);
+ if( !_ir.phi(n) || _a.hidden(n) || _seen.put(n,Boolean.TRUE)!=null ) continue;
+ if( _a.postAlloc() ) {
+ if( !packed ) { indent(); packed=true; }
+ _text.append(_a.phiName(n)).append(':').append(_a.register(n)).append(',');
+ } else {
+ indent(); field(_a.phiName(n),5); _text.append(' ').append(_a.register(n));
+ if( _ir.kind(n)!=Kind.PARM ) {
+ _text.append(" = phi( ");
+ for( int j=1; j<_ir.nIns(n); j++ ) {
+ if( j>1 ) _text.append(',');
+ N def=_ir.in(n,j);
+ _text.append(def==null ? "___" : "N"+_ir.id(def));
+ }
+ _text.append(" )");
+ }
+ _text.append('\n');
+ }
+ }
+ if( packed ) { _text.setLength(_text.length()-1); _text.append('\n'); }
+ for( int i=0; i<_ir.nOuts(bb); i++ ) {
+ N n=_ir.out(bb,i);
+ if( n!=null && !_ir.phi(n) ) instruction(n,blocks,idx);
+ }
+ }
+
+ private void instruction(N n, ArrayList<N> blocks, int idx) {
+ if( n==null || _a.hidden(n) || _ir.kind(n)==Kind.CPROJ ) return;
+ if( _ir.region(n) && _ir.kind(n)!=Kind.FUN ) {
+ if( _a.encoded() ) return;
+ // A successor may be reached by falling through empty blocks.
+ for( int j=idx+1; j<blocks.size(); j++ ) {
+ N next=blocks.get(j);
+ if( next==n ) return;
+ if( _ir.nOuts(next)>1 ) break;
+ }
+ address(_address++); field("??",_a.defaultSize()*2);
+ _text.append(" "); field("JMP",5); _text.append(' ').append(label(n)).append('\n');
+ return;
+ }
+ if( _seen.put(n,Boolean.TRUE)!=null ) return;
+ if( _ir.kind(n)==Kind.PROJ ) {
+ if( _a.encoded() ) return;
+ indent(); _text.append(" "); field(_a.register(n),5);
+ _text.append(" // ").append(_ir.label(n)).append('\n');
+ return;
+ }
+ int offset=_a.offset(n), size=Math.max(0,_a.size(n)), width=_a.defaultSize();
+ if( offset>=0 ) _address=offset;
+ String args=_a.operands(n);
+ String[] lines=(args.startsWith("\n") ? args.substring(1) : args).split("\n",-1);
+ int done=0;
+ for( int row=0; row<lines.length || done<size; row++ ) {
+ if( row>0 && done>=size && row==lines.length-1 && lines[row].isEmpty() ) break;
+ address(_address);
+ int count=_a.bytes()==null ? 0 : Math.min(width,size-done);
+ bytes(_address,count,_a.littleEndian());
+ field("",(width-count)*2); _text.append(" ");
+ if( row==0 ) { field(_a.op(n),5); _text.append(' '); }
+ if( row<lines.length ) field(lines[row],row==0 ? 30 : 0);
+ if( row==0 ) {
+ String comment=_a.comment(n);
+ if( comment!=null ) _text.append(" // ").append(comment);
+ }
+ _text.append('\n');
+ done+=count; _address+=count;
+ if( _a.bytes()==null ) {
+ // Before encoding, multiline machine expansions still show
+ // every instruction; byte lengths are not yet available.
+ if( row==0 ) { _address+=size; done=size; }
+ }
+ if( count==0 && row>=lines.length-1 ) break;
+ }
+ if( !_ir.control(n) )
+ for( int i=0; i<_ir.nOuts(n); i++ ) {
+ N proj=_ir.out(n,i);
+ if( proj!=null && _ir.kind(proj)==Kind.PROJ && _ir.input0(proj)==n ) instruction(proj,blocks,idx);
+ }
+ }
+ private void indent() { field("",7+_a.defaultSize()*2); }
+ private void field(String text, int width) {
+ _text.append(text);
+ if( width>text.length() ) _text.append(" ".repeat(width-text.length()));
+ }
+ private void address(int address) { _text.append("%04x ".formatted(address)); }
+ private void bytes(int offset, int count, boolean little) {
+ byte[] bytes=_a.bytes();
+ for( int i=0; i<count; i++ ) {
+ int idx=offset+(little ? i : count-i-1);
+ if( idx<0 || idx>=bytes.length ) _text.append("??");
+ else _text.append("%02x".formatted(bytes[idx]&255));
+ }
+ }
+
+ /** Metadata captured during encoding; printing never computes a type's layout. */
+ public record Data(Object identity, String type, int alignment, int size) {}
+ public record Pool(String name, byte[] bytes, int offset, int address, ArrayList<Data> entries) {}
+
+ public static String pool(Pool pool) {
+ StringBuilder sb=new StringBuilder("--- "+pool.name()+" ------\n");
+ ArrayList<Data> entries=new ArrayList<>(pool.entries());
+ entries.sort(Comparator.comparingInt(Data::alignment).reversed());
+ IdentityHashMap<Object,Boolean> seen=new IdentityHashMap<>();
+ int offset=pool.offset(), address=pool.address();
+ for( Data entry : entries ) {
+ if( seen.put(entry.identity(),Boolean.TRUE)!=null ) continue;
+ sb.append("%04x ".formatted(address));
+ for( int i=0; i<entry.size(); i++ )
+ sb.append(offset+i<pool.bytes().length ? "%02x".formatted(pool.bytes()[offset+i]&255) : "??");
+ sb.append('\t').append(entry.type()).append('\n');
+ offset+=entry.size(); address+=entry.size();
+ }
+ return sb.toString();
+ }
+}
diff --git a/print/src/main/java/com/seaofnodes/print/AssemblyAdapter.java b/print/src/main/java/com/seaofnodes/print/AssemblyAdapter.java
new file mode 100644
index 00000000..a6aaf8d8
--- /dev/null
+++ b/print/src/main/java/com/seaofnodes/print/AssemblyAdapter.java
@@ -0,0 +1,26 @@
+package com.seaofnodes.print;
+
+import java.util.ArrayList;
+
+/** Machine facts for a listing, independent of traversal and column formatting. */
+public abstract class AssemblyAdapter<N extends BaseNode<N>> {
+ public final IRAdapter<N> ir;
+ protected AssemblyAdapter(IRAdapter<N> ir) { this.ir=ir; }
+ public abstract ArrayList<N> blocks();
+ public abstract String op(N n);
+ public abstract String operands(N n);
+ public abstract String register(N n);
+ public String comment(N n) { return null; }
+ public String phiName(N n) { return ir.label(n); }
+ public boolean hidden(N n) { return false; }
+ public boolean labelBlock(N n) { return true; }
+ public boolean postAlloc() { return false; }
+ public boolean encoded() { return false; }
+ public boolean prologue(N fun) { return false; }
+ public int defaultSize() { return 4; }
+ public boolean littleEndian() { return true; }
+ public int offset(N n) { return -1; }
+ public int size(N n) { return 1; }
+ public byte[] bytes() { return null; }
+ public ArrayList<ASMPrinter.Pool> pools(int codeEnd) { return new ArrayList<>(); }
+}
diff --git a/print/src/main/java/com/seaofnodes/print/BaseNode.java b/print/src/main/java/com/seaofnodes/print/BaseNode.java
new file mode 100644
index 00000000..cc196b8c
--- /dev/null
+++ b/print/src/main/java/com/seaofnodes/print/BaseNode.java
@@ -0,0 +1,37 @@
+package com.seaofnodes.print;
+
+/** Common read-only node view for debug printing and graph capture. */
+public abstract class BaseNode<N extends BaseNode<N>> {
+ // Allocated by the compiler. Copies receive a new ID from that same allocator.
+ public int _nid;
+ protected BaseNode(int nid) { _nid=nid; }
+
+ public abstract N in(int i);
+ public abstract int nIns();
+ public abstract N out(int i);
+ public abstract int nOuts();
+ public int nDeps() { return 0; }
+ public N dep(int i) { throw new IndexOutOfBoundsException(i); }
+
+ public String label() { return getClass().getSimpleName(); }
+ public String typeName() { return null; }
+ public boolean isDead() { return false; }
+ public boolean isCFG() { return false; }
+ public String glabel() { return label(); }
+ public String comment() { return null; }
+ public final String uniqueName() { return label()+_nid; }
+
+ // %0, %1, ... name input slots; %% prints a literal percent sign.
+ protected String format() { return null; }
+ protected ExprPrinter<N> _print1(ExprPrinter<N> p) {
+ String fmt=format();
+ return fmt==null ? p.p(label()) : p.prt(self(),fmt);
+ }
+ // Short constants return null to expand their value on each appearance.
+ protected String repeatName() { return label(); }
+
+ @SuppressWarnings("unchecked")
+ private N self() { return (N)this; }
+ public final String print() { return ExprPrinter.print(self()); }
+ @Override public final String toString() { return print(); }
+}
diff --git a/print/src/main/java/com/seaofnodes/print/ExprPrinter.java b/print/src/main/java/com/seaofnodes/print/ExprPrinter.java
new file mode 100644
index 00000000..129be470
--- /dev/null
+++ b/print/src/main/java/com/seaofnodes/print/ExprPrinter.java
@@ -0,0 +1,56 @@
+package com.seaofnodes.print;
+
+import java.util.IdentityHashMap;
+
+/** One expression's buffer and repeat tracking, shared by recursive node hooks. */
+public final class ExprPrinter<N extends BaseNode<N>> {
+ private final StringBuilder _text=new StringBuilder();
+ private final IdentityHashMap<N,Boolean> _seen=new IdentityHashMap<>();
+ private ExprPrinter() {}
+
+ public static <N extends BaseNode<N>> String print(N node) {
+ return new ExprPrinter<N>().n(node).toString();
+ }
+
+ public ExprPrinter<N> n(N node) {
+ if( node==null ) return p("____");
+ if( node.isDead() ) return p(node.uniqueName()).p(":DEAD");
+ if( _seen.put(node,Boolean.TRUE)!=null ) {
+ String ref=node.repeatName();
+ if( ref!=null ) return p(ref);
+ }
+ node._print1(this);
+ return this;
+ }
+
+ /** Input slots are decimal numbers. Missing inputs print like null inputs. */
+ public ExprPrinter<N> prt(N node, String fmt) {
+ for( int i=0; i<fmt.length(); i++ ) {
+ char c=fmt.charAt(i);
+ if( c!='%' ) { p(c); continue; }
+ if( ++i==fmt.length() ) throw new IllegalArgumentException("Trailing % in "+fmt);
+ if( fmt.charAt(i)=='%' ) { p('%'); continue; }
+ int start=i;
+ while( i<fmt.length() && fmt.charAt(i)>='0' && fmt.charAt(i)<='9' ) i++;
+ if( start==i ) throw new IllegalArgumentException("Expected input slot in "+fmt);
+ int slot=Integer.parseInt(fmt.substring(start,i--));
+ n(node==null || slot>=node.nIns() ? null : node.in(slot));
+ }
+ return this;
+ }
+
+ public ExprPrinter<N> p(Object text) { _text.append(text); return this; }
+ public ExprPrinter<N> open() { return p('('); }
+ public ExprPrinter<N> close() { return p(')'); }
+ public ExprPrinter<N> unchar() { return unchar(1); }
+ public ExprPrinter<N> unchar(int n) { _text.setLength(_text.length()-n); return this; }
+ /** Remove a trailing separator only when it matches. */
+ public ExprPrinter<N> unchar(char c) {
+ return _text.length()>0 && _text.charAt(_text.length()-1)==c ? unchar() : this;
+ }
+ public ExprPrinter<N> unchar(String suffix) {
+ int start=_text.length()-suffix.length();
+ return start>=0 && _text.indexOf(suffix,start)==start ? unchar(suffix.length()) : this;
+ }
+ @Override public String toString() { return _text.toString(); }
+}
diff --git a/print/src/main/java/com/seaofnodes/print/IRAdapter.java b/print/src/main/java/com/seaofnodes/print/IRAdapter.java
new file mode 100644
index 00000000..06b169b0
--- /dev/null
+++ b/print/src/main/java/com/seaofnodes/print/IRAdapter.java
@@ -0,0 +1,34 @@
+package com.seaofnodes.print;
+
+/** Read-only facts about a chapter's graph. Needed only for structured IR dumps. */
+public abstract class IRAdapter<N extends BaseNode<N>> {
+ public enum Kind { DATA, CONSTANT, CTRL, START, STOP, REGION, LOOP, PHI, PARM,
+ MULTI, PROJ, CPROJ, FUN, RETURN, CALL, CALL_END, UNIT }
+ public int id(N n) { return n._nid; }
+ public String label(N n) { return n.label(); }
+ public int nIns(N n) { return n.nIns(); }
+ public N in(N n, int i) { return n.in(i); }
+ public int nOuts(N n) { return n.nOuts(); }
+ public N out(N n, int i) { return n.out(i); }
+ public abstract Kind kind(N n);
+ public boolean dead(N n) { return n.isDead(); }
+ public String type(N n) { String t=n.typeName(); return t==null ? "" : t; }
+ public int index(N n) { return id(n); }
+ public String signature(N fun) { return label(fun); }
+ public String functionName(N fun) { return label(fun); }
+ public String inputMark(N n, N def) { return " "; }
+ public boolean global(N n) { return kind(n)==Kind.CONSTANT; }
+ public int inputColumns() { return 4; }
+
+ public final N input0(N n) { return n == null || nIns(n) == 0 ? null : in(n,0); }
+ public final boolean control(N n) {
+ if( n == null ) return false;
+ return switch(kind(n)) {
+ case CTRL, START, STOP, REGION, LOOP, CPROJ, FUN, RETURN, CALL, CALL_END, UNIT -> true;
+ default -> false;
+ };
+ }
+ public final boolean phi(N n) { return n != null && (kind(n) == Kind.PHI || kind(n) == Kind.PARM); }
+ public final boolean projection(N n) { return n != null && (kind(n) == Kind.PROJ || kind(n) == Kind.CPROJ); }
+ public final boolean region(N n) { return n != null && (kind(n) == Kind.REGION || kind(n) == Kind.LOOP || kind(n) == Kind.FUN); }
+}
diff --git a/print/src/main/java/com/seaofnodes/print/IRPrinter.java b/print/src/main/java/com/seaofnodes/print/IRPrinter.java
new file mode 100644
index 00000000..bfe1cb05
--- /dev/null
+++ b/print/src/main/java/com/seaofnodes/print/IRPrinter.java
@@ -0,0 +1,293 @@
+package com.seaofnodes.print;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.IdentityHashMap;
+import static com.seaofnodes.print.IRAdapter.Kind;
+
+/** Shared diagnostic traversal. All ownership, ordering and visited state is local. */
+public final class IRPrinter<N extends BaseNode<N>> {
+ private final IRAdapter<N> _a;
+ public IRPrinter(IRAdapter<N> adapter) { _a = adapter; }
+
+ public record Unit<N>(String name, N start, N stop, ArrayList<N> functions) {}
+
+ public String program(N start, N stop, ArrayList<Unit<N>> units) {
+ StringBuilder sb=new StringBuilder(line(start));
+ ArrayList<N> constants=new ArrayList<>();
+ IdentityHashMap<N,Boolean> graph=new IdentityHashMap<>(), global=new IdentityHashMap<>();
+ globals(start,graph,constants);
+ for( N n : constants ) global.put(n,Boolean.TRUE);
+ // GVN can share a floating expression of constants between functions.
+ for( N n : sorted(graph) )
+ if( !_a.global(n) && floatingGlobal(n,global) ) constants.add(n);
+ constants.sort(Comparator.comparingInt(_a::id));
+ for( N n : constants ) sb.append(line(n));
+ ArrayList<Unit<N>> ordered=new ArrayList<>(units);
+ ordered.sort(Comparator.comparing(u -> u.name()==null ? "" : u.name()));
+ for( Unit<N> unit : ordered ) {
+ if( unit.start()!=null ) {
+ if( _a.dead(unit.start()) ) continue;
+ sb.append("\n=== ").append(unit.name()).append(" ===\n").append(line(unit.start()));
+ ArrayList<N> ps=new ArrayList<>();
+ for( int i=0; i<_a.nOuts(unit.start()); i++ ) {
+ N p=_a.out(unit.start(),i);
+ if( _a.projection(p) ) ps.add(p);
+ }
+ ps.sort(Comparator.comparingInt(_a::index));
+ for( N p : ps ) sb.append(line(p));
+ }
+ ArrayList<N> funs=new ArrayList<>(unit.functions());
+ funs.sort(Comparator.comparingInt(_a::id));
+ for( N fun : funs ) if( !_a.dead(fun) ) sb.append(function(fun,global));
+ sb.append(line(unit.stop()));
+ }
+ return sb.append(line(stop)).toString();
+ }
+
+ // Function-pointer constants can be attached to Returns rather than Start.
+ private void globals(N n, IdentityHashMap<N,Boolean> seen, ArrayList<N> constants) {
+ if( n==null || seen.put(n,Boolean.TRUE)!=null ) return;
+ if( _a.global(n) && !_a.dead(n) ) constants.add(n);
+ for( int i=0; i<_a.nIns(n); i++ ) globals(_a.in(n,i),seen,constants);
+ for( int i=0; i<_a.nOuts(n); i++ ) globals(_a.out(n,i),seen,constants);
+ }
+
+ private boolean floatingGlobal(N n, IdentityHashMap<N,Boolean> global) {
+ if( n==null ) return true;
+ Boolean old=global.get(n);
+ if( old!=null ) return old;
+ global.put(n,Boolean.FALSE); // Stop incomplete data cycles.
+ if( _a.dead(n) || _a.kind(n)!=Kind.DATA || _a.input0(n)!=null || _a.nIns(n)==0 ) return false;
+ for( int i=1; i<_a.nIns(n); i++ )
+ if( !floatingGlobal(_a.in(n,i),global) ) return false;
+ global.put(n,Boolean.TRUE);
+ return true;
+ }
+
+ public String line(N n) {
+ if( n == null ) return "";
+ StringBuilder sb = new StringBuilder();
+ sb.append("%4d %-7.7s ".formatted(_a.id(n),_a.label(n)));
+ if( _a.dead(n) ) return sb.append("DEAD\n").toString();
+ for( int i=0; i<_a.nIns(n); i++ ) {
+ N def = _a.in(n,i);
+ sb.append(def == null ? "____" : "%4d".formatted(_a.id(def))).append(_a.inputMark(n,def));
+ }
+ int cols = _a.inputColumns();
+ for( int i=_a.nIns(n); i<cols; i++ ) sb.append(" ");
+ sb.append(" [[ ");
+ for( int i=0; i<_a.nOuts(n); i++ ) {
+ N use = _a.out(n,i);
+ sb.append(use == null ? "____ " : "%4d ".formatted(_a.id(use)));
+ }
+ for( int i=_a.nOuts(n); i<cols+2-Math.max(_a.nIns(n),cols); i++ ) sb.append(" ");
+ return sb.append(" ]] ").append(_a.type(n)).append('\n').toString();
+ }
+
+ /** A bounded backward slice, including the headers of boundary projections/Phis. */
+ public String prettyPrint(N root, int depth) {
+ IdentityHashMap<N,Boolean> nodes = new IdentityHashMap<>();
+ ArrayDeque<N> todo = new ArrayDeque<>();
+ if( root != null ) { nodes.put(root,Boolean.TRUE); todo.add(root); }
+ for( int d=0; d<depth && !todo.isEmpty(); d++ ) {
+ int count=todo.size();
+ while( count-->0 ) {
+ N n=todo.remove();
+ for( int i=0; i<_a.nIns(n); i++ ) {
+ N def=_a.in(n,i);
+ if( def != null && nodes.put(def,Boolean.TRUE)==null ) todo.add(def);
+ }
+ }
+ }
+ for( N n : new ArrayList<>(nodes.keySet()) )
+ if( _a.phi(n) || _a.projection(n) ) {
+ N head=_a.input0(n);
+ if( head != null ) nodes.put(head,Boolean.TRUE);
+ }
+ return render(nodes);
+ }
+
+ /** Dump one function without traversing the caller/callee linkage. */
+ public String function(N fun) {
+ return function(fun,new IdentityHashMap<>());
+ }
+
+ private String function(N fun, IdentityHashMap<N,Boolean> global) {
+ IdentityHashMap<N,Boolean> nodes = new IdentityHashMap<>();
+ collect(fun,fun,nodes);
+ nodes.keySet().removeIf(n -> Boolean.TRUE.equals(global.get(n)));
+ // Floating expressions of constants need not have a forward path from
+ // the function header. Include their definitions from the owned uses.
+ for( N n : new ArrayList<>(nodes.keySet()) )
+ // Parm alternatives are arguments in the callers, not local defs.
+ if( _a.kind(n)!=Kind.PARM )
+ for( int i=0; i<_a.nIns(n); i++ ) collectData(_a.in(n,i),nodes,global);
+ return "\n--- "+_a.signature(fun)+"----------------------\n"+
+ render(nodes)+"--- "+_a.functionName(fun)+" ----------------------\n";
+ }
+
+ private void collect(N n, N owner, IdentityHashMap<N,Boolean> seen) {
+ if( n == null || _a.dead(n) || seen.containsKey(n) || _a.global(n) ) return;
+ Kind k=_a.kind(n);
+ if( (k==Kind.FUN && n!=owner) || (k==Kind.PARM && _a.input0(n)!=owner) ) return;
+ if( k==Kind.START || k==Kind.STOP || k==Kind.UNIT ) return;
+ if( k==Kind.CALL_END && !seen.containsKey(_a.input0(n)) ) return;
+ seen.put(n,Boolean.TRUE);
+ if( k!=Kind.RETURN )
+ for( int i=0; i<_a.nOuts(n); i++ ) collect(_a.out(n,i),owner,seen);
+ }
+
+ private void collectData(N n, IdentityHashMap<N,Boolean> seen, IdentityHashMap<N,Boolean> global) {
+ if( n==null || seen.containsKey(n) || _a.dead(n) || _a.global(n) || _a.control(n) ||
+ Boolean.TRUE.equals(global.get(n)) ) return;
+ N head=_a.input0(n);
+ if( _a.control(head) && !seen.containsKey(head) ) return;
+ seen.put(n,Boolean.TRUE);
+ for( int i=0; i<_a.nIns(n); i++ ) collectData(_a.in(n,i),seen,global);
+ }
+
+ private ArrayList<N> sorted(IdentityHashMap<N,Boolean> nodes) {
+ ArrayList<N> ns = new ArrayList<>(nodes.keySet());
+ ns.sort(Comparator.comparingInt(_a::id));
+ return ns;
+ }
+
+ /** Finish CFG RPO before placing data; data backedges cannot reorder blocks. */
+ private String render(IdentityHashMap<N,Boolean> nodes) {
+ ArrayList<N> ns=sorted(nodes), post=new ArrayList<>();
+ IdentityHashMap<N,Boolean> seen=new IdentityHashMap<>();
+ // Start from CFG roots, then cover disconnected/partially built components.
+ for( N n : ns )
+ if( _a.control(n) && !hasPred(n,nodes) ) controlPost(n,nodes,seen,post);
+ for( N n : ns )
+ if( _a.control(n) ) controlPost(n,nodes,seen,post);
+ Collections.reverse(post);
+ IdentityHashMap<N,Integer> blocks=new IdentityHashMap<>();
+ ArrayList<ArrayList<N>> body=new ArrayList<>();
+ for( int i=0; i<post.size(); i++ ) { blocks.put(post.get(i),i); body.add(new ArrayList<>()); }
+ if( body.isEmpty() ) body.add(new ArrayList<>());
+ for( N n : ns )
+ if( !_a.control(n) ) body.get(block(n,nodes,blocks)).add(n);
+ StringBuilder sb=new StringBuilder();
+ IdentityHashMap<N,Boolean> emitted=new IdentityHashMap<>();
+ for( int i=0; i<body.size(); i++ ) {
+ if( i<post.size() ) {
+ N cfg=post.get(i);
+ sb.append('\n');
+ emitDefs(cfg,nodes,emitted,sb);
+ emitLine(cfg,emitted,sb);
+ // A Phi's backedge belongs in the body, never ahead of the Phi.
+ for( N n : body.get(i) ) if( _a.phi(n) ) emitLine(n,emitted,sb);
+ }
+ for( N n : body.get(i) ) emit(n,nodes,emitted,sb);
+ }
+ // Include unanchored Phis and dead/incomplete nodes exactly once.
+ for( N n : ns ) emitLine(n,emitted,sb);
+ return sb.toString();
+ }
+
+ private boolean edge(N def, N use) {
+ if( !_a.control(use) || _a.kind(use)==Kind.FUN || _a.kind(def)==Kind.RETURN ) return false;
+ if( _a.region(use) || _a.kind(use)==Kind.STOP ) {
+ for( int i=_a.kind(use)==Kind.STOP ? 0 : 1; i<_a.nIns(use); i++ )
+ if( _a.in(use,i)==def ) return true;
+ return false;
+ }
+ return _a.input0(use)==def;
+ }
+ private boolean hasPred(N n, IdentityHashMap<N,Boolean> nodes) {
+ for( int i=0; i<_a.nIns(n); i++ ) {
+ N def=_a.in(n,i);
+ // The loop entry determines its root status, not its backedge.
+ if( _a.kind(n)==Kind.LOOP && i>1 ) break;
+ if( def!=null && nodes.containsKey(def) && _a.control(def) && edge(def,n) ) return true;
+ }
+ return false;
+ }
+ private void controlPost(N n, IdentityHashMap<N,Boolean> nodes,
+ IdentityHashMap<N,Boolean> seen, ArrayList<N> post) {
+ if( seen.put(n,Boolean.TRUE)!=null ) return;
+ ArrayList<N> uses=new ArrayList<>();
+ for( int i=0; i<_a.nOuts(n); i++ ) {
+ N use=_a.out(n,i);
+ if( use!=null && nodes.containsKey(use) && edge(n,use) ) uses.add(use);
+ }
+ uses.sort(Comparator.comparingInt(_a::id));
+ for( N use : uses ) controlPost(use,nodes,seen,post);
+ post.add(n);
+ }
+ private int block(N n, IdentityHashMap<N,Boolean> nodes, IdentityHashMap<N,Integer> blocks) {
+ Integer old=blocks.get(n);
+ if( old!=null ) return old;
+ if( n==null || !nodes.containsKey(n) || _a.kind(n)==Kind.CONSTANT ) return 0;
+ blocks.put(n,0);
+ int b=0;
+ N head=_a.input0(n);
+ if( _a.control(head) || _a.projection(n) ) b=block(head,nodes,blocks);
+ else for( int i=0; i<_a.nIns(n); i++ ) b=Math.max(b,block(_a.in(n,i),nodes,blocks));
+ blocks.put(n,b);
+ return b;
+ }
+ private void emitDefs(N n, IdentityHashMap<N,Boolean> nodes, IdentityHashMap<N,Boolean> emitted,
+ StringBuilder sb) {
+ for( int i=0; i<_a.nIns(n); i++ ) emit(_a.in(n,i),nodes,emitted,sb);
+ }
+ private void emit(N n, IdentityHashMap<N,Boolean> nodes, IdentityHashMap<N,Boolean> emitted,
+ StringBuilder sb) {
+ if( n==null || emitted.containsKey(n) || !nodes.containsKey(n) || _a.control(n) || _a.phi(n) ) return;
+ if( _a.projection(n) && nodes.containsKey(_a.input0(n)) ) {
+ N head=_a.input0(n);
+ if( !emitted.containsKey(head) ) emit(head,nodes,emitted,sb);
+ if( emitted.containsKey(n) ) return;
+ }
+ emitted.put(n,Boolean.TRUE);
+ emitDefs(n,nodes,emitted,sb);
+ sb.append(line(n));
+ projections(n,nodes,emitted,sb);
+ }
+ private void emitLine(N n, IdentityHashMap<N,Boolean> emitted, StringBuilder sb) {
+ if( emitted.put(n,Boolean.TRUE)==null ) sb.append(line(n));
+ }
+ private void projections(N n, IdentityHashMap<N,Boolean> nodes, IdentityHashMap<N,Boolean> emitted,
+ StringBuilder sb) {
+ ArrayList<N> ps=new ArrayList<>();
+ for( int i=0; i<_a.nOuts(n); i++ ) {
+ N p=_a.out(n,i);
+ if( p!=null && nodes.containsKey(p) && _a.kind(p)==Kind.PROJ && _a.input0(p)==n ) ps.add(p);
+ }
+ ps.sort(Comparator.comparingInt(_a::index));
+ for( N p : ps ) emitLine(p,emitted,sb);
+ }
+
+ /** Scheduling already determines order; inspect it without sorting compiler arrays. */
+ public String scheduled(ArrayList<N> blocks) {
+ StringBuilder sb=new StringBuilder();
+ IdentityHashMap<N,Boolean> emitted=new IdentityHashMap<>();
+ for( N block : blocks ) {
+ if( block==null ) continue;
+ sb.append('\n');
+ emitLine(block,emitted,sb);
+ for( int i=0; i<_a.nOuts(block); i++ ) {
+ N use=_a.out(block,i);
+ if( _a.phi(use) ) emitLine(use,emitted,sb);
+ }
+ for( int i=0; i<_a.nOuts(block); i++ ) {
+ N use=_a.out(block,i);
+ if( use!=null && !_a.control(use) ) {
+ emitLine(use,emitted,sb);
+ ArrayList<N> ps=new ArrayList<>();
+ for( int j=0; j<_a.nOuts(use); j++ ) {
+ N p=_a.out(use,j);
+ if( p!=null && _a.kind(p)==Kind.PROJ && _a.input0(p)==use ) ps.add(p);
+ }
+ ps.sort(Comparator.comparingInt(_a::index));
+ for( N p : ps ) emitLine(p,emitted,sb);
+ }
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/print/src/test/java/com/seaofnodes/print/PrinterTest.java b/print/src/test/java/com/seaofnodes/print/PrinterTest.java
new file mode 100644
index 00000000..d63cc6cb
--- /dev/null
+++ b/print/src/test/java/com/seaofnodes/print/PrinterTest.java
@@ -0,0 +1,153 @@
+package com.seaofnodes.print;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import static com.seaofnodes.print.IRAdapter.Kind;
+
+/** Small independent contracts for the shared printers. Run with assertions enabled. */
+public class PrinterTest {
+ static class N extends BaseNode<N> {
+ final String name;
+ final Kind kind;
+ final ArrayList<N> ins=new ArrayList<>(), outs=new ArrayList<>();
+ N(int id, String name, Kind kind, N... defs) {
+ super(id);
+ this.name=name; this.kind=kind;
+ for( N def : defs ) add(def);
+ }
+ @Override public N in(int i) { return ins.get(i); }
+ @Override public int nIns() { return ins.size(); }
+ @Override public N out(int i) { return outs.get(i); }
+ @Override public int nOuts() { return outs.size(); }
+ @Override public String label() { return name; }
+ @Override protected String repeatName() { return kind==Kind.CONSTANT ? null : name; }
+ @Override protected String format() { return ins.isEmpty() ? null : "(%0+%1)"; }
+ void add(N def) { ins.add(def); if( def!=null ) def.outs.add(this); }
+ }
+ static class Adapter extends IRAdapter<N> {
+ @Override public Kind kind(N n) { return n.kind; }
+ }
+
+ public static void main(String[] args) {
+ expression(); loop(); functions(); assembly();
+ System.out.println("Shared printer contracts passed");
+ }
+ static void expression() {
+ N one=new N(1,"1",Kind.CONSTANT), add=new N(2,"Add",Kind.DATA,one,one);
+ assert add.print().equals("(1+1)");
+ add.ins.set(1,add);
+ assert add.print().equals("(1+Add)");
+ // A failed callback must not leave another print's repeat state behind.
+ N bad=new N(3,"Bad",Kind.DATA) {
+ @Override protected ExprPrinter<N> _print1(ExprPrinter<N> p) { throw new IllegalStateException(); }
+ };
+ try { bad.print(); } catch( IllegalStateException expected ) {}
+ assert add.print().equals("(1+Add)");
+ N fmt=new N(4,"Format",Kind.DATA) {
+ @Override protected ExprPrinter<N> _print1(ExprPrinter<N> p) { return p.prt(this,"%10 %% %0 %12"); }
+ };
+ for( int i=0; i<11; i++ ) fmt.add(one);
+ assert fmt.print().equals("1 % 1 ____") : fmt.print();
+ N empty=new N(5,"Empty",Kind.DATA) {
+ @Override protected ExprPrinter<N> _print1(ExprPrinter<N> p) { return p.open().unchar(',').unchar(", ").close(); }
+ };
+ assert empty.print().equals("()");
+ }
+ static void loop() {
+ N fun=new N(1,"Fun",Kind.FUN);
+ N loop=new N(2,"Loop",Kind.LOOP,null,fun);
+ N phi=new N(3,"Phi",Kind.PHI,loop,null);
+ N branch=new N(4,"If",Kind.CTRL,loop,phi);
+ N yes=new N(5,"True",Kind.CPROJ,branch), no=new N(6,"False",Kind.CPROJ,branch);
+ N left=new N(7,"Left",Kind.CTRL,yes), right=new N(8,"Right",Kind.CTRL,no);
+ N merge=new N(9,"Merge",Kind.REGION,null,left,right);
+ N value=new N(10,"Add",Kind.DATA,null,phi);
+ phi.add(value); loop.add(merge);
+ N exit=new N(11,"Return",Kind.RETURN,merge,value);
+ // A call linkage must not bring another function's body into this dump.
+ N foreign=new N(12,"Other",Kind.FUN,exit);
+ new N(13,"Foreign",Kind.RETURN,foreign);
+ var nodes=new ArrayList<>(Arrays.asList(fun,loop,phi,branch,yes,no,left,right,merge,value,exit));
+ var inputs=new ArrayList<ArrayList<N>>(); var outputs=new ArrayList<ArrayList<N>>();
+ for( N n : nodes ) { inputs.add(new ArrayList<>(n.ins)); outputs.add(new ArrayList<>(n.outs)); }
+ IRPrinter<N> printer=new IRPrinter<>(new Adapter());
+ String text=printer.function(fun);
+ assert text.equals(printer.function(fun));
+ assert !text.contains("Foreign");
+ int li=row(text,"Loop"), pi=row(text,"Phi"), le=row(text,"Left"), ri=row(text,"Right"), mi=row(text,"Merge");
+ assert li>=0 && pi>li && le>pi && ri>pi && mi>le && mi>ri : text;
+ String[] lines=text.split("\n");
+ for( int i=0; i<lines.length; i++ )
+ if( lines[i].matches("\\s*2 Loop.*") ) assert lines[i+1].matches("\\s*3 Phi.*") : text;
+ for( int i=0; i<nodes.size(); i++ ) {
+ N n=nodes.get(i);
+ assert n.ins.equals(inputs.get(i)) && n.outs.equals(outputs.get(i));
+ int count=0;
+ for( String line : lines ) if( line.matches("\\s*"+n._nid+" "+n.name+" .*")) count++;
+ assert count==1 : n.name+" appears "+count+" times\n"+text;
+ }
+ assert printer.prettyPrint(null,2).isEmpty();
+ }
+ static int row(String text,String name) { return text.indexOf(" "+name+" "); }
+
+ static void functions() {
+ N start=new N(1,"Start",Kind.START), stop=new N(2,"Stop",Kind.STOP);
+ N one=new N(3,"One",Kind.CONSTANT,start);
+ N sum=new N(4,"Sum",Kind.DATA,null,one,one);
+ N caller=new N(5,"Caller",Kind.FUN,start), callee=new N(6,"Callee",Kind.FUN,start);
+ N arg=new N(7,"Arg",Kind.PARM,caller);
+ N load=new N(8,"Load",Kind.DATA,caller,arg);
+ N parm=new N(9,"Parm",Kind.PARM,callee,load);
+ N ret1=new N(10,"Ret1",Kind.RETURN,caller,load,sum);
+ N ret2=new N(11,"Ret2",Kind.RETURN,callee,parm,sum);
+ stop.add(ret1); stop.add(ret2);
+ IRPrinter<N> printer=new IRPrinter<>(new Adapter());
+ var funs=new ArrayList<>(Arrays.asList(caller,callee));
+ var units=new ArrayList<IRPrinter.Unit<N>>();
+ units.add(new IRPrinter.Unit<>("",null,null,funs));
+ String text=printer.program(start,stop,units);
+ for( N n : Arrays.asList(start,stop,one,sum,caller,callee,arg,load,parm,ret1,ret2) ) {
+ long count=text.lines().filter(line -> line.matches("\\s*"+n._nid+" .*" )).count();
+ assert count==1 : n.name+" appears "+count+" times\n"+text;
+ }
+ assert row(text,"Sum")<row(text,"Caller") : text;
+ assert !printer.function(callee).contains(" Load ") : text;
+
+ N alloc=new N(12,"New",Kind.MULTI,caller), ptr=new N(13,"Ptr",Kind.PROJ,alloc);
+ String scheduled=printer.scheduled(new ArrayList<>(Arrays.asList(caller,ret1)));
+ assert row(scheduled,"Ptr")>row(scheduled,"New") : scheduled;
+ }
+
+ static void assembly() {
+ N fun=new N(1,"Fun",Kind.FUN), op=new N(2,"op",Kind.DATA,fun);
+ N ret=new N(3,"Return",Kind.RETURN,fun,op);
+ boolean[] encoded={true};
+ var adapter=new AssemblyAdapter<N>(new Adapter()) {
+ @Override public ArrayList<N> blocks() { return new ArrayList<>(Arrays.asList(fun,ret)); }
+ @Override public String op(N n) { return n.name; }
+ @Override public String operands(N n) { return n==op ? "\nfirst\nsecond" : ""; }
+ @Override public String register(N n) { return "r0"; }
+ @Override public boolean hidden(N n) { return n==ret; }
+ @Override public boolean encoded() { return encoded[0]; }
+ @Override public int defaultSize() { return 2; }
+ @Override public int size(N n) { return 4; }
+ @Override public int offset(N n) { return 0; }
+ @Override public byte[] bytes() { return encoded[0] ? new byte[]{1,2,3,4} : null; }
+ @Override public boolean littleEndian() { return false; }
+ };
+ ASMPrinter<N> printer=new ASMPrinter<>(adapter);
+ String asm=printer.print();
+ assert asm.equals(printer.print());
+ assert asm.contains("0201") && asm.contains("0403") && asm.contains("second") : asm;
+ encoded[0]=false;
+ asm=new ASMPrinter<>(adapter).print();
+ assert asm.contains("first") && asm.contains("second") : asm;
+ Object type=new Object();
+ var entries=new ArrayList<ASMPrinter.Data>();
+ entries.add(new ASMPrinter.Data(type,"Pair",1,2));
+ entries.add(new ASMPrinter.Data(type,"Pair",1,2));
+ String pool=ASMPrinter.pool(new ASMPrinter.Pool("Constants",new byte[]{5,6},0,16,entries));
+ assert pool.indexOf("Pair")==pool.lastIndexOf("Pair") : pool;
+ assert pool.contains("0010 0506") : pool;
+ }
+}