package com.seaofnodes.simple.codegen; import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.print.IRPrinter; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.*; import java.util.*; /** * Instruction encodings *

* This class holds the encoding bits, plus the relocation information needed * to move the code to a non-zero offset, or write to an external file (ELF). *

* There are also a bunch of generic utilities for managing bits and bytes * common in all encodings. */ public class Encoding implements com.seaofnodes.isa.CodeSink { // Top-level program graph structure public final CodeGen _code; // CFG Nodes in RPO order, per-encoding. public Ary _cfg; // Instruction bytes. The registers are encoded already. Relocatable data // is in a fixed format depending on the kind of relocation. // - RIP-relative offsets into the same chunk are just encoded with their // chunk relative offsets; no relocation info is required. // - Fixed address targets in the same encoding will be correct for a zero // base; moving the entire encoding requires adding the new base address. // - RIP-relative to external chunks have a zero offset; the matching // relocation info will be used to patch the correct value. public final BAOS _bits = new BAOS(); // Instructions / encodings public final BAOS _cpool = new BAOS(); // Constant r/o pool public final BAOS _sdata = new BAOS(); // Static r/w pool public final HashMap _bigCons = new HashMap<>(); public final StaticData _data; public final HashMap _externalAddresses = new HashMap<>(); // Big Constant relocation info. public static class Relo { public final Node _op; public final Type _t; // Constant type public final int _align; // Log alignment, captured for diagnostic display public final int _structSize; // Capture layout during encoding, not printing public final byte _off; // Offset from start of opcode public final byte _elf; // ELF relocation type, e.g. 2/PC32 public int _target; // Where constant is finally placed public int _opStart; // Opcode start Relo( Node op, Type t, byte off, byte elf ) { _op=op; _t=t; _off=off; _elf=elf; _align = t.alignment(); _structSize = t instanceof TypeStruct ts ? ts.size() : 0; _readOnly = readOnly(t); } public boolean readOnly() { return _readOnly; } private final boolean _readOnly; private static boolean readOnly(Type t) { // Class initializers still write their fields, even when every // final value is constant. Their storage must remain writable. return !(t instanceof TypeStruct ts) || (!Parser.startsClzPrefix(ts._name) && ts.isConstant()); } } Encoding( CodeGen code ) { _code = code; _data = new StaticData(this); } // Shortcut to the defining register public short reg(Node n) { return _code._regAlloc.regnum(n); } public int read1(int idx) { byte[] buf = _bits.buf(); return (buf[idx]&0xFF); } public int read2(int idx) { byte[] buf = _bits.buf(); return (buf[idx]&0xFF) | (buf[idx+1]&0xFF) <<8; } public int read4(int idx) { byte[] buf = _bits.buf(); return (buf[idx]&0xFF) | (buf[idx+1]&0xFF) <<8 | (buf[idx+2]&0xFF)<<16 | (buf[idx+3]&0xFF)<<24; } public long read8(int idx) { return (read4(idx) & 0xFFFFFFFFL) | read4(idx+4); } // This buffer is invalid/moving until after all encodings are written public byte[] bits() { return _bits.buf(); } // 4 byte little-endian write public void patch4( int idx, int val ) { byte[] buf = _bits.buf(); buf[idx ] = (byte)(val ); buf[idx+1] = (byte)(val>> 8); buf[idx+2] = (byte)(val>>16); buf[idx+3] = (byte)(val>>24); } static void padN(int n, BAOS bits) { while( (bits.size()+n-1 & -n) > bits.size() ) bits.write(0); } // Convenience for writing log-N static void addN( int log, long x, BAOS bits ) { for( int i=0; i < 1<>= 8; } } public Encoding add1( int op ) { addN(0,op,_bits); return this; } public Encoding add2( int op ) { addN(1,op,_bits); return this; } public Encoding add4( int op ) { addN(2,op,_bits); return this; } public Encoding add8(long op ) { addN(3,op,_bits); return this; } private final static int SENTINEL = -1; private final IntHashMap _opStart = new IntHashMap(SENTINEL); private final IntHashMap _opLen = new IntHashMap(SENTINEL); // Read op start for n public int opStart(Node n) { return _opStart.get(n._nid); } // Read op length for n public byte opLen (Node n) { return (byte)_opLen.get(n._nid); } // Set op start for n public void opStart(Node n, int off) { assert off != SENTINEL; _opStart.put(n._nid,off); } // Set op start for n public void opStartAdd(Node n, int off) { if( off==0 ) return; int old = _opStart.get(n._nid); assert old != SENTINEL && old+off != SENTINEL; _opStart.put(n._nid,old+off); } // Set op length for n public void opLen(Node n, byte len) { assert len != SENTINEL; _opLen.put(n._nid,len); } // Nodes need "relocation" patching; things done after code is placed. // Record src and dst Nodes. private final HashMap _internals = new HashMap<>(); // Source is a Call, destination in the Fun. public Encoding relo( CallNode call ) { FunNode fun = _code.link(call.tfp()); if( fun != null && !_code.owns(fun) ) _externals.put(call,fun._name); else _internals.put(call,fun); return this; } // Code address as a constant public Encoding relo( FunPtrNode fptr ) { TypeFunPtr tfp = (TypeFunPtr)fptr._type; FunNode fun = _code.link(tfp); if( fun != null && !_code.owns(fun) ) _externals.put(fptr,fun._name); else _internals.put(fptr,fun); return this; } // Local jump public void jump( CFGNode jmp, CFGNode dst ) { _internals.put(jmp,dst.uctrlSkipEmpty()); } // External references only located by Strings a link-time public final HashMap _externals = new HashMap<>(); public Encoding external( Node n, String extern ) { _externals.put(n,extern); return this; } public final HashMap _externData = new HashMap<>(); public void externalData(Node n, String name) { _externData.put(n,name); } // Store t as a 32/64 bit constant in the code space; generate RIP-relative // addressing to load it. Type is stored in either the .rodata or .data. public void largeConstant( Node relo, Type t, int off, int elf ) { // TODO: Any-old struct in the cpool assert t.isConstant() || (t instanceof TypeStruct ts && Parser.startsClzPrefix(ts._name)); assert (byte)off == off; assert (byte)elf == elf; _bigCons.put(relo,new Relo(relo,t,(byte)off,(byte)elf)); } // -------------------------------------------------- Encoding encode( ) { // Basic block layout: negate branches to keep blocks in-order; insert // unconditional jumps. Attempt to keep backwards branches taken, // forwards not-taken (this is the default prediction on most // hardware). Layout is still Reverse Post Order but with more // restrictions. basicBlockLayout( ); // Write encoding bits in order into a big byte array. // Record opcode start and length. writeEncodings(); // Short-form RIP-relative support: replace long encodings with short // encodings and compact the code, changing all the offsets. compactShortForm(); // Place pool objects and record relocations within their contents. _data.write(); // Patch RIP-relative and local encodings now. patchLocalRelocations(); return this; } // -------------------------------------------------- // Basic block layout: negate branches to keep blocks in-order; insert // unconditional jumps. Attempt to keep backwards branches taken, forwards // not-taken (this is the default prediction on most hardware). Layout is // still Reverse Post Order but with more restrictions. private void basicBlockLayout( ) { IdentityHashMap> rpos = new IdentityHashMap<>(); _cfg = new Ary<>(CFGNode.class); rpos.put(_code._start.loop(),_cfg); BitSet visit = _code.visit(); // Do them all except the s Ary clinits = new Ary<>(FunNode.class); FunNode entry = null; String entryName = _code.entryClinitName(); for( FunNode fun : _code._linker ) { if( fun != null && !fun.isDead() && _code.owns(fun) ) { if( fun.isClz() ) { if( fun._name.equals(entryName) ) entry = fun; else clinits.add(fun); } else { int x = _cfg._len; _rpo_cfg(fun,visit,rpos); assert _cfg.at(x) instanceof ReturnNode; } } } // Now the s last, so when reversed they become first. The // program entry is last of all, so it lands at offset zero. for( FunNode clinit : clinits ) { int x = _cfg._len; _rpo_cfg(clinit,visit,rpos); assert _cfg.at(x) instanceof ReturnNode; } if( entry != null ) { int x = _cfg._len; _rpo_cfg(entry,visit,rpos); assert _cfg.at(x) instanceof ReturnNode; } // Reverse in-place for( int i=0; i< _cfg.size()>>1; i++ ) _cfg.swap(i,_cfg.size()-1-i); visit.clear(); } private void _rpo_cfg(CFGNode bb, BitSet visit, IdentityHashMap> rpos ) { if( bb==null || visit.get(bb._nid) ) return; visit.set(bb._nid); CFGNode next = bb.uctrl(); // Loops run an inner "rpo_cfg", then append the entire loop body in // place. This keeps loop bodies completely contained, although if // Some Day Later we have real profile data we ought to arrange the // branch orderings based on frequency. if( bb instanceof LoopNode loop ) { Ary body = new Ary<>(CFGNode.class); // Private RPO for the loop rpos.put(loop.loop(),body); // Find it via loop tree _rpo_cfg(next,visit,rpos); // RPO the loop body.add(loop); // Include loop last (first in RPO) Ary outer = rpos.get(loop.cfg(1).loop()); outer.addAll(body); // Append to original CFG return; } // IfNodes visit 2 sides, and may choose to reorder them in the RPO if( bb instanceof IfNode iff ) { // Pick out the T/F projections CProjNode t = iff.cproj(0); CProjNode f = iff.cproj(1); // Invert the branch or not if( shouldInvert(t,f,iff.loopDepth()) ) { iff.negate(); t.invert(); f.invert(); CProjNode tmp=f; f=t; t=tmp; // Swap t/f } // Whichever side is visited first becomes last in the RPO. With // no loops, visit the False side last (so True side first) so that // when the False RPO visit returns, the IF is immediately next. // When the RPO is reversed, the fall-through path will always be // following the IF. _rpo_cfg(t,visit,rpos); // True side first next = f; // False side last } // If the *next* BB has already been visited, and we are not already a // jump, we may need an unconditional forwards jump here Ary rpo = rpos.get(bb.loop()); if( next!=null && visit.get(next._nid) && !(bb instanceof IfNode) ) { boolean needJump = next instanceof LoopNode // Empty blocks from an IF will invert the IF and backwards // branch to the loop head. If not empty, or not an IF // will need a jump. ? (bb.nOuts()>1 || !(bb.cfg0() instanceof IfNode) ) // Forwards jump. If all the blocks, in RPO order, to our // target are empty, we will fall in and not need a jump. : !backwardsEmptyScan(rpo,next); if( needJump ) { CFGNode jmp = _code._mach.jump(); jmp._ltree = bb._ltree; jmp.setDefX(0,bb); next.setDef(next._inputs.find(bb),jmp); rpo.add(jmp); } } _rpo_cfg(next,visit,rpos); rpo.add(bb); } // Should this test be inverted? private static boolean shouldInvert(CFGNode t, CFGNode f, int bld) { int tld = t.loopDepth(), fld = f.loopDepth(); // These next two are symmetric and can happen in any order; if `tld < // bld` is true, then `fld < bld` must be false, or else both directions // exit the loop... and the IF test would not be in the loop. // true to exit a loop usually (and false falls into Yet Another Loop // Block), invert if false is taking an empty backwards branch. if( tld < bld ) return forwardsEmptyScan(f,bld); // false to exit a loop, normally invert to true (except empty back) if( fld < bld ) return !forwardsEmptyScan(t,bld); // Not exiting the loop; staying at this depth (or going deeper loop). // Jump/true to an empty backwards block if( forwardsEmptyScan(t,bld) ) return false; if( forwardsEmptyScan(f,bld) ) return true ; // Fall/false into a full block, Jump/true to an empty block. if( f.nOuts()>1 && t.nOuts()==1 ) return false; if( t.nOuts()>1 && f.nOuts()==1 ) return true ; // Jump to a merge point, assuming other things are jumping there as well if( f.out(0) instanceof RegionNode ) return true ; if( t.out(0) instanceof RegionNode ) return false; // Everything else equal, use pre-order return t._pre > f._pre; } // Do we continue forwards from 'c' to the Loop back edge, without doing // anything else? Then this branch can directly jump to the loop head. private static boolean forwardsEmptyScan( CFGNode c, int bld ) { if( c.nOuts()!=1 || c.loopDepth()!=bld ) return false; return c.uctrl() instanceof RegionNode cfg && ((cfg instanceof LoopNode && cfg._ltree==c._ltree) || forwardsEmptyScan(cfg,bld)); } // Is the CFG from "next" to the end empty? This means jumping to "next" // will naturally fall into the end. private static boolean backwardsEmptyScan(Ary rpo, CFGNode next) { for( int i=rpo._len-1; rpo.at(i)!=next; i-- ) if( rpo.at(i).nOuts()!=1 ) return false; return true; } // -------------------------------------------------- // Write encoding bits in order into a big byte array. // Record opcode start and length. // Current function is used by the spill-op encodings to query the stack // frame layout and get the spill offsets. private void writeEncodings() { for( CFGNode bb : _cfg ) { if( !(bb instanceof MachNode mach0) ) opStart(bb, _bits.size()); else if( bb instanceof FunNode ) { opStart(bb, _bits.size()); mach0.encoding( this ); opLen(bb, (byte) (_bits.size() - opStart(bb))); } for( Node n : bb._outputs ) { if( n instanceof MachNode mach && !(n instanceof FunNode) && // FunPtrs are encoding in the block of their control, not // the block of the ReturnNode input - which is also a CFG. !(n instanceof FunPtrNode && bb instanceof ReturnNode) ) { opStart(n, _bits.size()); mach.encoding( this ); opLen(n, (byte) (_bits.size() - opStart(n))); } } } } // -------------------------------------------------- // Short-form RIP-relative support: replace short encodings with long // encodings and expand the code, changing all the offsets. private void compactShortForm() { int len = _cfg._len; int[] oldStarts = new int[len]; for( int i=0; i 0 ) { // If no short-form ops, nothing to do here int end = _bits.size(); byte[] bits = new byte[end+grow]; for( int i=len-1; i>=0; i-- ) { int start = oldStarts[i]; if( start==0 && i>1 ) continue; int oldStart = oldStarts[i]; int newStart = opStart(_cfg.at(i)); System.arraycopy(_bits.buf(),oldStart,bits,newStart,end-start); end = start; } _bits.set(bits,bits.length); } } // -------------------------------------------------- // Patch local encodings now void patchLocalRelocations() { // Walk the local code-address relocations for( Node src : _internals.keySet() ) { int start = opStart(src); Node dst = _internals.get(src); // If function is entirely dead, only the function pointer remains // and, it can only be used to test against zero or equals to // another function pointer... i.e., there Is No Code Here. int target = dst == null ? start : opStart(dst); ((RIPRelSize)src).patch(this, start, opLen(src), target - start); } } // Emit a single scalar as bits static void addN( int log, Type t, BAOS bits ) { long x = switch( t ) { case TypeInteger ti -> ti.value(); case TypeFloat tf -> log==3 ? Double.doubleToRawLongBits( tf.value()) : Float.floatToRawIntBits((float)tf.value()); case TypeFunPtr tfp -> 0; // These need to be relocated case TypeMemPtr tmp -> 0; // These need to be relocated default -> { if( t==Type.NIL ) yield 0; else throw Utils.TODO(); } }; addN(log,x,bits); } // A series of libc/external calls that Simple can link against in a JIT. // Since no runtime in the JVM process, using magic numbers for the CPU // emulators to pick up on. public static int SENTINEL_CALLOC = -4; public static int SENTINEL_WRITE = -8; int externalAddress(String name) { Integer address=_externalAddresses.get(name); if( address!=null ) return address; return switch(name) { case "calloc" -> SENTINEL_CALLOC; case "write" -> SENTINEL_WRITE; default -> throw new IllegalArgumentException("Unresolved external function '"+name+"'"); }; } void patchGlobalRelocations() { for( Node src : _externData.keySet() ) { String name = _externData.get(src); Integer target = _code._externDataAddresses.get(name); if( target==null ) throw new IllegalArgumentException("Unresolved C data symbol '"+name+"'"); int start = opStart(src); ((RIPRelSize)src).patch(this,start,opLen(src),target-start); } for( Node src : _externals.keySet() ) { int start = opStart(src); String dst = _externals.get(src); int target = externalAddress(dst); ((RIPRelSize)src).patch(this, start, opLen(src), target - start); } } }