diff options
485 files changed, 43722 insertions, 187 deletions
@@ -4,7 +4,7 @@ .DEFAULT_GOAL := tests # Named snapshots follow teaching order, not alphabetical order. Numbered # chapters remain comparison baselines while the split is being extracted. -NAMED_CHAPTERS := chapter-sccp chapter-interprocedural-sccp chapter-incomplete-types chapter-construction chapter-escape +NAMED_CHAPTERS := chapter-sccp chapter-interprocedural-sccp chapter-incomplete-types chapter-construction chapter-escape chapter-modules CHAPTERS ?= $(sort $(patsubst %/pom.xml,%,$(wildcard chapter[0-9][0-9]/pom.xml chapter[0-9][0-9][a-z]/pom.xml))) $(NAMED_CHAPTERS) ACTIONS := tests tags release lib CHAPTER_TARGETS := $(foreach action,$(ACTIONS),$(addsuffix /$(action),$(CHAPTERS))) diff --git a/README.ja.md b/README.ja.md index b544e639..014f0add 100644 --- a/README.ja.md +++ b/README.ja.md @@ -72,6 +72,7 @@ Simple 言語のスタイルは、C または Java のサブセットに倣っã� * [型が確定する前の SSA 構築](chapter-incomplete-types/README.ja.md) * [オブジェクトの構築と非公開メモリ](chapter-construction/README.ja.md) * [エスケープ解析と未知の呼び出し元](chapter-escape/README.ja.md) +* [モジュールと遅延クラス初期化](chapter-modules/README.ja.md) ## 章をまたぐビルド @@ -82,6 +82,7 @@ baselines during extraction. The first named snapshots are: * [SSA Before Types Are Known](chapter-incomplete-types/README.md) * [Construction and Private Memory](chapter-construction/README.md) * [Escape Analysis and Unknown Callers](chapter-escape/README.md) +* [Modules and Lazy Class Initialization](chapter-modules/README.md) ## Building across chapters diff --git a/chapter-construction/README.ja.md b/chapter-construction/README.ja.md index 86104de0..2fa83280 100644 --- a/chapter-construction/README.ja.md +++ b/chapter-construction/README.ja.md @@ -11,10 +11,18 @@ フィールドごとの非公開メモリを次の関数に渡します。構築が完了した時点で、 `EscapeNode` が初期化済みのフィールドを公開メモリに合流させます。 -全ての実行経路で必須フィールドが初期化されていることを確認します。 +パーサーはコンストラクタの出口で、必須フィールドが初期化されていることを確認します。 +この検査は SCCP より前なので、後の解析で安全と分かる呼び出しでも拒否する場合があります。 構築中の `self` を通常の関数に渡したり、オブジェクトに格納したりする -ことは、この章では禁止します。別名や呼び出しを通じた公開の追跡は、 -次のエスケープ解析の章で扱います。この境界は引き続き検討します。 +ことは、この章では禁止します。次のエスケープ解析の章でもこの制限は残ります。 +インライン化後に安全性を確認してヘルパー呼び出しを許す案は、まだ実装していません。 + +未完成のオブジェクトをコンストラクタ内で読むことはできますが、読むフィールドは +すでに初期化されている必要があります。他のフィールドはまだ未設定でも構いません。 +final フィールドも構築中はローカル値として再代入でき、ループでは通常の Phi を使います。 +Store はヘルパーの出口で生成します。構築後の書き換え、通常のローカル `val` への再代入、 +共有する関数宣言の置き換えは禁止します。 +詳細と提案中の規則は [英語版](README.md#helper-calls-and-final-fields-during-construction) を参照してください。 モジュール、クラスの遅延初期化、分割コンパイルはまだ導入しません。 詳しい説明、制限、実行例は [英語版](README.md) を参照してください。 diff --git a/chapter-construction/README.md b/chapter-construction/README.md index c61bd7b7..f0ea4222 100644 --- a/chapter-construction/README.md +++ b/chapter-construction/README.md @@ -75,7 +75,7 @@ including their existing integer range widening. For example: ```simple struct Counter { - int x; + val x = 0; new Counter = { int n -> while (x < n) x++; }; }; return new Counter(arg).x; @@ -86,6 +86,17 @@ performs no field Store on each trip. At the constructor exit, the compiler stores the final value into the new object. This is the result scalar replacement would give us, built directly from the construction rules. +Even final data fields can change during construction; they become final when +the constructor finishes. `x = 1; x = 2;` leaves the local value 2 for the exit +Store. In the loop above, `x++` updates a local Phi, not memory. No sequence of +final field Stores needs cleaning up inside the loop. + +A constant default does not make a field shared by every instance: a later +constructor may change it. Inferred field types therefore describe the value's +family, not just its initial constant. Ordinary local `val` bindings remain +final. Shared function declarations are also fixed; a constructor cannot replace +one of those functions. + Private memory is still useful at the helper boundaries. `NewNode` produces the pointer and its initial private memory. The hidden initializer computes defaults as local values and emits ordinary initializing Stores at exit. The @@ -109,18 +120,17 @@ initialized length are published before the array is returned. Required non-null fields have no legal zero default. Reading one before it is written is an error. - ## Publish only after completion `EscapeNode` publishes one field. It consumes control, public memory, private memory, and `self`; its result joins the field's value into the public alias. Publication has no machine instruction, but its memory edges keep initializing -Stores before later Loads and Stores. Every field is published independently, +Stores before later Loads and Stores. Every field is published independently, and scheduling may well interleave the initializing writes from many unrelated allocations and public memory updates. Before publication, every required field must be initialized on every live -path. This is rejected when `arg` can be zero: +path. This constructor is rejected: ```simple struct Box { @@ -130,10 +140,44 @@ struct Box { return new Box(arg); ``` -This is rejected because `int[] data` requires a non-null initial value, and if -`arg` is null, `data` is never set not-null. This check is based on the type -of every field at the constructor exit; if type analysis (e.g. SCCP) determines -the field is set, the code is good; if not then not. +`int[] data` requires a non-null initial value. If `n` is zero, `data` is never +set. The current parser follows explicit missing-initialization values through +the constructor's merges and checks its exit. This happens before SCCP: even +calling this constructor only with `new Box(1)` does not rescue it. Allowing +later type analysis to prove completion would require deferring this check. + + +## Helper calls and *final* fields during construction + +Constructor code may read the unfinished object, but never an uninitialized +field. Ordinary code must not observe it before construction finishes. + +We would like to allow helper calls when optimization proves they do not expose +the unfinished object. Inlining could turn a helper's field accesses into local +values. After optimization reaches a fixed point, each final field would have +a final Store: before that line, accesses use local values; after it, the field +can be read but never written. Mutable fields can still be written later. + +This is a proposed relaxation, not implemented in this snapshot. The parser +currently rejects passing `self` to an ordinary method before inlining can +prove it safe. For example, we would like this constructor to work: + +```simple +struct Vec { + int _max; + u8[~] !_buf; // Not-null, must be set before reading + + new Vec = { -> + _buf = new u8[1]; + clear(); // Would be safe after inlining; currently rejected + }; + val clear = { -> + // Keep at most 1K of storage in a cleared Vec + if( _buf# > 1024 ) _buf = new u8[1024]; + _max=0; + }; +}; +``` ## Build and check @@ -153,3 +197,6 @@ make tests CHAPTERS=chapter-construction CTAGS= defaults, completion errors, forward fields and methods, private loops, native execution, and ARM/RISC-V execution. The inherited language, memory, lattice, and backend suites run alongside it. +`FinalConstructionTest` checks repeated assignments and loops over final fields, +reads through methods, rejection of later writes, and native execution. It also +checks that the loop's field Stores are outside the loop. diff --git a/chapter-construction/src/main/java/com/seaofnodes/simple/Parser.java b/chapter-construction/src/main/java/com/seaofnodes/simple/Parser.java index 5ce7c410..dabfb6a2 100644 --- a/chapter-construction/src/main/java/com/seaofnodes/simple/Parser.java +++ b/chapter-construction/src/main/java/com/seaofnodes/simple/Parser.java @@ -267,7 +267,7 @@ public class Parser { // 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(new Kind.Func()); + _scope.push(new Kind.Func(_privateFun!=null)); ctrl(fun); // Scope control from function // Unknown callers supply whole memory; field aliases are discovered lazily. _scope.mem(new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,con(TypeMem.BOT)).peephole()); @@ -768,6 +768,7 @@ public class Parser { // The alloc() call added the allocation scope !_scope.canInit(def) ) throw error("Cannot reassign final '"+name+"'"); + checkSharedField(def); // Parse assignment expression checkCaptureWrite(def); @@ -869,6 +870,14 @@ public class Parser { throw error("a not-null/non-zero expression"); t = expr._type; if( !xfinal ) t = inferredType(t); // Widen if not final + // A final field is still a local variable during construction. + // Its constant default is a value, not a one-value declaration + // type: later assignments and loop Phis may change it. + else if( _scope.inConstructor() && !(t instanceof TypeFunPtr) ) { + Type wide=inferredType(t); + t = t instanceof TypeMemPtr ptr && ptr.notNull() + ? TypeMemPtr.make((byte)2,((TypeMemPtr)wide)._obj,false).withAccess(ptr._ro) : wide; + } } // expr is a constant function if( t instanceof TypeFunPtr && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) { @@ -948,7 +957,10 @@ public class Parser { Var v = _scope.var(index++); if( v.isFRef() ) continue; Node value = _scope.in(v); - boolean constant = v._final && v._init && value._type.isConstant(); + // Only function declarations are shared across instances. A final + // data field can change in a named or inline constructor, so a + // constant default does not justify folding every future read. + boolean constant = v._final && v._init && value._type instanceof TypeFunPtr && value._type.isConstant(); Field field = Field.make(v._name,v.type(),_code.getALIAS(),v._final,constant); shape=shape.add(field); TYPES.put(_nestedType,TypeMemPtr.make(shape)); @@ -1452,8 +1464,9 @@ public class Parser { if( name!=null ) { Var n = _scope.lookup(name); if( n != null && !(n.type() instanceof TypeMemPtr) ) { - if( n._final ) + if( n._final && !_scope.canInit(n) ) throw error("Cannot reassign final '"+n._name+"'"); + checkSharedField(n); Node expr = liftExpr(peep(new AddNode(_scope.in(n),con(delta))),n.type(),true); _scope.update(n,expr); return expr; @@ -1545,8 +1558,9 @@ public class Parser { if( ch==0 ) // Normal primary, check for postfix updates return parsePostfix(rvalue); // Assign-update direct into Scope - if( var._final ) + if( var._final && !_scope.canInit(var) ) throw error("Cannot reassign final '"+var._name+"'"); + checkSharedField(var); checkCaptureWrite(var); Node op = opAssign(ch,rvalue, var.type() ); _scope.update(var,op); @@ -1600,6 +1614,24 @@ public class Parser { throw error("Cannot assign captured variable '"+var._name+"' in an initializer"); } + // Inline constructors have local field slots too, but shared function + // declarations have no instance slot to publish. Do not silently discard + // an assignment to one of those declarations. + private void checkSharedField(Var var) { + if( !_scope.canInit(var) ) return; + if( _scope.kind(var) instanceof Kind.Define ) { + checkPrivateWrite(var._name); + return; + } + for( int i=_scope.depth()-1; i>0; i-- ) + if( _scope._kinds.at(i) instanceof Kind.Alloc alloc ) { + Field field=alloc._tmp._obj.field(var._name); + if( field!=null && field._one ) + throw error("Cannot reassign shared function '"+var._name+"'"); + return; + } + } + // 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. @@ -1980,6 +2012,7 @@ public class Parser { if( v.type() instanceof TypeFunPtr ctor && ctor.ret() instanceof TypeMem ) throw error("A constructor is only called by new"); if( matchOpx('=','=') ) { + checkPrivateWrite(name); Node value=parseAsgn().keep(); checkPublication(value); _scope.update(v,liftExpr(value,v.type(),false)); @@ -1990,6 +2023,7 @@ public class Parser { throw error("Cannot read uninitialized field '"+name+"'"); char ch=_lexer.matchOperAssign(); if( ch!=0 ) { + checkPrivateWrite(name); Node op=opAssign(ch,value,v.type()); checkPublication(op); _scope.update(v,op); @@ -2000,6 +2034,12 @@ public class Parser { return parsePostfix(value); } + private void checkPrivateWrite(String name) { + Field field=((TypeMemPtr)TYPES.get(_nestedType))._obj.field(name); + if( field!=null && field._one ) + throw error("Cannot reassign shared function '"+name+"'"); + } + // 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 ) { diff --git a/chapter-construction/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter-construction/src/main/java/com/seaofnodes/simple/node/ScopeNode.java index 3c1e9776..f65a822d 100644 --- a/chapter-construction/src/main/java/com/seaofnodes/simple/node/ScopeNode.java +++ b/chapter-construction/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -39,7 +39,10 @@ public class ScopeNode extends Node { // Basic block scoping public static class Block extends Kind { } // Function scope - public static class Func extends Kind { } + public static class Func extends Kind { + public final boolean _constructor; + public Func(boolean constructor) { _constructor=constructor; } + } // Struct definition, mapping types to fields public static class Define extends Kind { public Define(TypeMemPtr tmp) { _tmp=tmp; } public final TypeMemPtr _tmp; } // Struct allocation, mapping values to fields @@ -137,15 +140,25 @@ public class ScopeNode extends Node { } - // Allocation fields live in the scope immediately outside the constructor - // block. Nested blocks retain that permission; surrounding bindings do not. + // Fields are writable local values until construction ends, even when the + // published field is final. Ordinary methods and final local variables do + // not inherit that permission. This also decides which loop Phis we need. public boolean canInit(Var v) { - for( int i=_kinds._len-1; i>0; i-- ) - if( _kinds.at(i) instanceof Kind.Alloc ) + for( int i=_kinds._len-1; i>=0; i-- ) { + Kind k=_kinds.at(i); + if( k instanceof Kind.Alloc ) return v._idx >= _kinds.at(i-1)._lexSize && v._idx < _kinds.at(i)._lexSize; + if( k instanceof Kind.Define ) return kind(v)==k; + if( k instanceof Kind.Func fun ) + return fun._constructor && i>0 && _kinds.at(i-1) instanceof Kind.Define def && kind(v)==def; + } return false; } + private boolean loopVar(Var v) { + return !v._final || (canInit(v) && !v.type().isHighOrConst()); + } + public boolean inConstructor() { return _kinds.last() instanceof Kind.Define; } public boolean inAllocation () { return _kinds.last() instanceof Kind.Alloc ; } public boolean inFunction () { return _kinds.last() instanceof Kind.Func ; } @@ -294,7 +307,7 @@ public class ScopeNode extends Node { // Memory is one binding, with the same lazy loop Phi as scalar names. for( int i=1; i<nIns(); i++ ) - dup.addDef(loop && !_vars.at(i)._final ? this : in(i)); + dup.addDef(loop && loopVar(var(i)) ? this : in(i)); return dup; } @@ -356,7 +369,7 @@ public class ScopeNode extends Node { // Fill in the backedge of any inserted Phis void _endLoop( ScopeNode scope, Node back, Node exit ) { for( int i=1; i<nIns(); i++ ) { - if( _vars.at(i)._final ) continue; // Final vars did not get modified in the loop + if( !loopVar(var(i)) ) continue; 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); diff --git a/chapter-construction/src/main/java/com/seaofnodes/simple/type/Field.java b/chapter-construction/src/main/java/com/seaofnodes/simple/type/Field.java index 96e6d766..9bd85ba6 100644 --- a/chapter-construction/src/main/java/com/seaofnodes/simple/type/Field.java +++ b/chapter-construction/src/main/java/com/seaofnodes/simple/type/Field.java @@ -17,10 +17,10 @@ public class Field extends Type { public Type _t; // Unique memory alias, not sensibly part of a "type" but very convenient here. public int _alias; - // Field must be written to exactly once, no more, no less + // Field is immutable after construction; constructor locals may change public boolean _final; - // Field is final in the declaration; value is the same for all instances - // and will be moved to the class object + // Shared function declaration, with the same value for every instance. + // A final data field is still per-instance, even with a constant default. public boolean _one; private static final Ary<Field> FREE = new Ary<>(Field.class); diff --git a/chapter-construction/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/chapter-construction/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java new file mode 100644 index 00000000..e91dc384 --- /dev/null +++ b/chapter-construction/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java @@ -0,0 +1,63 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.StoreNode; +import org.junit.Test; +import static org.junit.Assert.*; + +public class FinalConstructionTest { + private static final String LOOP = """ + val step={int x -> x*2+1;}; + struct C { + val x=1; + new C={int n -> + x=0; x=1; + for(int i=0; i<n; i++) x=step(x); + }; + val get={->x;}; + }; + return new C(arg).get(); + """; + + @Test public void testFinalConstructorLocals() throws java.io.IOException { + var code=new CodeGen(LOOP).driver(CodeGen.Phase.LocalSched); + assertEquals("1",Eval2.eval(code,0)); + assertEquals("15",Eval2.eval(code,3)); + assertEquals("63",Eval2.eval(code,5)); + code._stop.walk(n -> { + if( n instanceof StoreNode st && st._name.equals("x") ) + assertEquals("Final field writes belong after the loop",0,st.cfg0().loopDepth()); + return null; + }); + String[] sources={ + "struct C { val x=1; new C={->x=1; x=2;}; }; return new C().x;", + "struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i<n;i++) self.x*=2;}; }; return new C(arg).x;", + "struct C { val x=1; new C={int n -> for(int i=0;i<n;i++) ++x;}; }; return new C(arg).x;", + "struct C { val x=1.5; new C={->x=2.5;}; }; return new C().x;", + "struct C { val x=\"a\"; new C={int n -> for(int i=0;i<n;i++) x=\"b\";}; }; return new C(arg).x[0];", + "struct C { val x=new int[1]; new C={->x=new int[2]; x=new int[3];}; }; return new C().x#;" + }; + String[] results={"2","8","4","2.5","98","3"}; + for( int i=0; i<sources.length; i++ ) + assertEquals(results[i],Eval2.eval(new CodeGen(sources[i]).driver(CodeGen.Phase.TypeCheck),3)); + + String[] rejected={ + "struct C { val x=1; }; val c=new C(); c.x=2; return c.x;", + "struct C { new C={->val x=1; x=2;}; }; return new C();", + "struct C { val f={->1;}; new C={->f={->2;};}; }; return new C().f();" + }; + for( String src : rejected ) { + try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(src); } + catch( Parser.ParseException e ) { assertTrue(e.getMessage(),e.getMessage().startsWith("Cannot")); } + } + + var dir=java.nio.file.Path.of("build/objs"); + java.nio.file.Files.createDirectories(dir); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.smp"),"val buildFinal={int arg ->"+LOOP+"};"); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.c"),""" + extern long long buildFinal(long long); + int main(void) { return buildFinal(0)!=1 || buildFinal(3)!=15 || buildFinal(5)!=63; } + """); + TestC.run("build/objs","finalConstruction","",-1); + } +} diff --git a/chapter-escape/README.ja.md b/chapter-escape/README.ja.md index 43bb4411..c496df4c 100644 --- a/chapter-escape/README.ja.md +++ b/chapter-escape/README.ja.md @@ -2,7 +2,8 @@ [English](README.md) | 日本語 -[前章: オブジェクトの構築と非公開メモリ](../chapter-construction/README.ja.md) +[前章: オブジェクトの構築と非公開メモリ](../chapter-construction/README.ja.md) | +[次: モジュールと遅延クラス初期化](../chapter-modules/README.ja.md) この章では、オブジェクトや関数のアドレスが、解析できない外部コードに届くかを追跡します。 関数のアドレスを既知の関数に渡すだけでは、未知の呼び出し元を残す必要はありません。 diff --git a/chapter-escape/README.md b/chapter-escape/README.md index 6098c72c..a1fda17b 100644 --- a/chapter-escape/README.md +++ b/chapter-escape/README.md @@ -2,14 +2,15 @@ English | [日本語](README.ja.md) -[Previous: Construction and Private Memory](../chapter-construction/README.md) +[Previous: Construction and Private Memory](../chapter-construction/README.md) | +[Next: Modules and Lazy Class Initialization](../chapter-modules/README.md) We run a simple form of escape analysis in this chapter, tracking both functions and some aliases across all calls and memory updates. One of the goals here is to discover when we've found *all* the callers of a function; at this point we can drop the conservative "unknown caller" input and run -constant propagation through a functions arguments. +constant propagation through a function's arguments. The preceding compiler kept an unknown caller for every surviving function address. That was safe, but it lost facts even when all uses stayed within the @@ -44,7 +45,7 @@ SCCP can then infer `base == 2`, including in its recursive body. The multiply by 2 can become a shift without inlining or cloning either recursive body - and we open ourselves up to further optimizations. -Passing `power` to an unknown caller, or passing it to a native function, +Passing `power` to an unknown callee, or passing it to a native function, changes the answer. The external caller may choose another base. The compiler must retain `power` and give its parameters their declared bounds. @@ -52,8 +53,8 @@ must retain `power` and give its parameters their declared bounds. ## Publishing an object and exposing it to unknown code The construction chapter's `EscapeNode` publishes initialized fields into -ordinary public memory. The completed object has not, yet, escaped; it -depends on what happens to the `self` pointer. +ordinary public memory. That alone does not expose the completed object to +unknown code; exposure depends on what happens to the `self` pointer. Escape analysis starts at the boundary with unknown code. Returning an object across that boundary exposes its field aliases. An exposed field can contain @@ -67,7 +68,7 @@ The same rule applies to arguments passed to native code. Native code may keep an address after the call returns, or use it on a path that never returns. `Stop` therefore also collects exposure directly from reachable native calls. A call to a known Simple function instead passes the values and memory to that -function's graph; its uses SCCP to determine what escapes. +function's graph; SCCP follows those uses to determine what escapes. This is a conservative analysis by field alias. Two instances of the same struct share their field aliases. Exposing one may therefore expose addresses @@ -75,7 +76,7 @@ stored in the same fields of another. It does not identify each allocation separately. Another way to think of this: this analysis is what we get "for free" from just -running SCCP. We extend and sharpen the lattice a litte more, to allow more +running SCCP. We extend and sharpen the lattice a little more, to allow more precise tracking. @@ -89,17 +90,15 @@ precise tracking. `XInt`, already used for function sets, represents both. An empty set means no address has been discovered; a full set is the conservative unknown case. Meeting summaries unions the sets. Dual types complement them, so the existing -lattice operations and tests still apply. The `XInt` utilities themselves just -track collections of immutable bits. They have no indirection, they are just -fly-weight wrappers around `int[]`s. The help support shared and immutable -`int[]`s so that these bit sets follow the same invariants as the rest of the -Types. +lattice operations and tests still apply. The `XInt` utilities operate directly +on shared, immutable `int[]` arrays; they add no wrapper object. Interning lets +these sets follow the same sharing rules as the rest of the Types. Stores record addresses carried by their values. Memory Phis combine those facts over live paths. `MemMergeNode` gathers the slices whose aliases are exposed, rather than making every locally stored callback externally callable. If an exposed field reveals another alias, that slice participates as well. In -short, we follow escaping values through memory using `Types` using the formal +short, we follow escaping values through memory using the existing `Node.compute` and `Type` machinery. The hidden constructor helpers still keep fields in local SSA values and use @@ -121,8 +120,8 @@ Start -> callable functions -> returns and exposed memory -> Stop ``` Before SCCP, parsing and pessimistic iteration keep conservative callers and -memory. During SCCP, functions lose their unknown-caller edges unless their -function pointer escapes and become unknown entry points. Concrete calls then +memory. During SCCP, functions other than the chosen entry points lose their +unknown-caller edges unless their function pointers escape. Concrete calls rebuild the known call graph. Stop combines the results and memory visible to external callers. Those @@ -133,22 +132,24 @@ parameter inputs. That function can reveal further addresses through its own calls and returns. The ordinary worklist repeats until types, exposure sets, and call edges agree - i.e. the normal SCCP reaches a fixed point. -If you have a `main` function, it starts discovery. Without `main`, the code -is assumed to be a library, and all public entry points are fair game. +An explicit `main`, or one synthesized for a top-level return, starts discovery. +Without one, the code is treated as a library: ordinary named functions outside `sys` +seed the unknown entry points. Module privacy refines this rule in the next +chapter. Once SCCP finishes, you might have ordinary public functions callable from anywhere, or functions called via their name (direct field load holding the -function point), or via a function pointer thats only used in a few locations, +function pointer), or via a function pointer that's only used in a few locations, or it might escape in a region and get mixed with other function pointers, or you might never call a function at all. If a function is getting called, but -never publically, its arguments can sharpen across the `meet` of all call sites -- perhaps allowing not-null-ness to be tracked precisely and avoid some -explicit tests. - -After optimization, `FinalizeIR` freezes the external summary - the escaped -values from `Start/Stop`, for tracking if a function gets a public entry in the -ELF file. `FinalizerIR` does a few more cleanups, including removing the -`Stop->Start` edge SCCP uses to track escapes across external calls. +never from unknown code, its arguments can sharpen across the `meet` of all +call sites, perhaps proving an argument non-null and avoiding explicit tests. + +After optimization, `FinalizeIR` freezes the external summary from `Start/Stop` +and removes the `Stop->Start` edge SCCP uses to track escapes across external +calls. This summary says which functions need unknown callers; it does not +control ELF symbol visibility. The current ELF writer emits every retained +named function as a global symbol. ## Construction boundary to review diff --git a/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java b/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java index f22b73f1..19dd1c29 100644 --- a/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java +++ b/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java @@ -267,7 +267,7 @@ public class Parser { // 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(new Kind.Func()); + _scope.push(new Kind.Func(_privateFun!=null)); ctrl(fun); // Scope control from function // Unknown callers supply whole memory; field aliases are discovered lazily. _scope.mem(new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,new ProjNode(_code._start,1,ScopeNode.MEM0).peephole()).peephole()); @@ -768,6 +768,7 @@ public class Parser { // The alloc() call added the allocation scope !_scope.canInit(def) ) throw error("Cannot reassign final '"+name+"'"); + checkSharedField(def); // Parse assignment expression checkCaptureWrite(def); @@ -869,6 +870,14 @@ public class Parser { throw error("a not-null/non-zero expression"); t = expr._type; if( !xfinal ) t = inferredType(t); // Widen if not final + // A final field is still a local variable during construction. + // Its constant default is a value, not a one-value declaration + // type: later assignments and loop Phis may change it. + else if( _scope.inConstructor() && !(t instanceof TypeFunPtr) ) { + Type wide=inferredType(t); + t = t instanceof TypeMemPtr ptr && ptr.notNull() + ? TypeMemPtr.make((byte)2,((TypeMemPtr)wide)._obj,false).withAccess(ptr._ro) : wide; + } } // expr is a constant function if( t instanceof TypeFunPtr && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) { @@ -948,7 +957,10 @@ public class Parser { Var v = _scope.var(index++); if( v.isFRef() ) continue; Node value = _scope.in(v); - boolean constant = v._final && v._init && value._type.isConstant(); + // Only function declarations are shared across instances. A final + // data field can change in a named or inline constructor, so a + // constant default does not justify folding every future read. + boolean constant = v._final && v._init && value._type instanceof TypeFunPtr && value._type.isConstant(); Field field = Field.make(v._name,v.type(),_code.getALIAS(),v._final,constant); shape=shape.add(field); TYPES.put(_nestedType,TypeMemPtr.make(shape)); @@ -1452,8 +1464,9 @@ public class Parser { if( name!=null ) { Var n = _scope.lookup(name); if( n != null && !(n.type() instanceof TypeMemPtr) ) { - if( n._final ) + if( n._final && !_scope.canInit(n) ) throw error("Cannot reassign final '"+n._name+"'"); + checkSharedField(n); Node expr = liftExpr(peep(new AddNode(_scope.in(n),con(delta))),n.type(),true); _scope.update(n,expr); return expr; @@ -1545,8 +1558,9 @@ public class Parser { if( ch==0 ) // Normal primary, check for postfix updates return parsePostfix(rvalue); // Assign-update direct into Scope - if( var._final ) + if( var._final && !_scope.canInit(var) ) throw error("Cannot reassign final '"+var._name+"'"); + checkSharedField(var); checkCaptureWrite(var); Node op = opAssign(ch,rvalue, var.type() ); _scope.update(var,op); @@ -1600,6 +1614,24 @@ public class Parser { throw error("Cannot assign captured variable '"+var._name+"' in an initializer"); } + // Inline constructors have local field slots too, but shared function + // declarations have no instance slot to publish. Do not silently discard + // an assignment to one of those declarations. + private void checkSharedField(Var var) { + if( !_scope.canInit(var) ) return; + if( _scope.kind(var) instanceof Kind.Define ) { + checkPrivateWrite(var._name); + return; + } + for( int i=_scope.depth()-1; i>0; i-- ) + if( _scope._kinds.at(i) instanceof Kind.Alloc alloc ) { + Field field=alloc._tmp._obj.field(var._name); + if( field!=null && field._one ) + throw error("Cannot reassign shared function '"+var._name+"'"); + return; + } + } + // 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. @@ -1980,6 +2012,7 @@ public class Parser { if( v.type() instanceof TypeFunPtr ctor && ctor.ret() instanceof TypeMem ) throw error("A constructor is only called by new"); if( matchOpx('=','=') ) { + checkPrivateWrite(name); Node value=parseAsgn().keep(); checkPublication(value); _scope.update(v,liftExpr(value,v.type(),false)); @@ -1990,6 +2023,7 @@ public class Parser { throw error("Cannot read uninitialized field '"+name+"'"); char ch=_lexer.matchOperAssign(); if( ch!=0 ) { + checkPrivateWrite(name); Node op=opAssign(ch,value,v.type()); checkPublication(op); _scope.update(v,op); @@ -2000,6 +2034,12 @@ public class Parser { return parsePostfix(value); } + private void checkPrivateWrite(String name) { + Field field=((TypeMemPtr)TYPES.get(_nestedType))._obj.field(name); + if( field!=null && field._one ) + throw error("Cannot reassign shared function '"+name+"'"); + } + // 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 ) { diff --git a/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java index 3c1e9776..f65a822d 100644 --- a/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java +++ b/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -39,7 +39,10 @@ public class ScopeNode extends Node { // Basic block scoping public static class Block extends Kind { } // Function scope - public static class Func extends Kind { } + public static class Func extends Kind { + public final boolean _constructor; + public Func(boolean constructor) { _constructor=constructor; } + } // Struct definition, mapping types to fields public static class Define extends Kind { public Define(TypeMemPtr tmp) { _tmp=tmp; } public final TypeMemPtr _tmp; } // Struct allocation, mapping values to fields @@ -137,15 +140,25 @@ public class ScopeNode extends Node { } - // Allocation fields live in the scope immediately outside the constructor - // block. Nested blocks retain that permission; surrounding bindings do not. + // Fields are writable local values until construction ends, even when the + // published field is final. Ordinary methods and final local variables do + // not inherit that permission. This also decides which loop Phis we need. public boolean canInit(Var v) { - for( int i=_kinds._len-1; i>0; i-- ) - if( _kinds.at(i) instanceof Kind.Alloc ) + for( int i=_kinds._len-1; i>=0; i-- ) { + Kind k=_kinds.at(i); + if( k instanceof Kind.Alloc ) return v._idx >= _kinds.at(i-1)._lexSize && v._idx < _kinds.at(i)._lexSize; + if( k instanceof Kind.Define ) return kind(v)==k; + if( k instanceof Kind.Func fun ) + return fun._constructor && i>0 && _kinds.at(i-1) instanceof Kind.Define def && kind(v)==def; + } return false; } + private boolean loopVar(Var v) { + return !v._final || (canInit(v) && !v.type().isHighOrConst()); + } + public boolean inConstructor() { return _kinds.last() instanceof Kind.Define; } public boolean inAllocation () { return _kinds.last() instanceof Kind.Alloc ; } public boolean inFunction () { return _kinds.last() instanceof Kind.Func ; } @@ -294,7 +307,7 @@ public class ScopeNode extends Node { // Memory is one binding, with the same lazy loop Phi as scalar names. for( int i=1; i<nIns(); i++ ) - dup.addDef(loop && !_vars.at(i)._final ? this : in(i)); + dup.addDef(loop && loopVar(var(i)) ? this : in(i)); return dup; } @@ -356,7 +369,7 @@ public class ScopeNode extends Node { // Fill in the backedge of any inserted Phis void _endLoop( ScopeNode scope, Node back, Node exit ) { for( int i=1; i<nIns(); i++ ) { - if( _vars.at(i)._final ) continue; // Final vars did not get modified in the loop + if( !loopVar(var(i)) ) continue; 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); diff --git a/chapter-escape/src/main/java/com/seaofnodes/simple/type/Field.java b/chapter-escape/src/main/java/com/seaofnodes/simple/type/Field.java index 96e6d766..9bd85ba6 100644 --- a/chapter-escape/src/main/java/com/seaofnodes/simple/type/Field.java +++ b/chapter-escape/src/main/java/com/seaofnodes/simple/type/Field.java @@ -17,10 +17,10 @@ public class Field extends Type { public Type _t; // Unique memory alias, not sensibly part of a "type" but very convenient here. public int _alias; - // Field must be written to exactly once, no more, no less + // Field is immutable after construction; constructor locals may change public boolean _final; - // Field is final in the declaration; value is the same for all instances - // and will be moved to the class object + // Shared function declaration, with the same value for every instance. + // A final data field is still per-instance, even with a constant default. public boolean _one; private static final Ary<Field> FREE = new Ary<>(Field.class); diff --git a/chapter-escape/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/chapter-escape/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java new file mode 100644 index 00000000..e91dc384 --- /dev/null +++ b/chapter-escape/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java @@ -0,0 +1,63 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.StoreNode; +import org.junit.Test; +import static org.junit.Assert.*; + +public class FinalConstructionTest { + private static final String LOOP = """ + val step={int x -> x*2+1;}; + struct C { + val x=1; + new C={int n -> + x=0; x=1; + for(int i=0; i<n; i++) x=step(x); + }; + val get={->x;}; + }; + return new C(arg).get(); + """; + + @Test public void testFinalConstructorLocals() throws java.io.IOException { + var code=new CodeGen(LOOP).driver(CodeGen.Phase.LocalSched); + assertEquals("1",Eval2.eval(code,0)); + assertEquals("15",Eval2.eval(code,3)); + assertEquals("63",Eval2.eval(code,5)); + code._stop.walk(n -> { + if( n instanceof StoreNode st && st._name.equals("x") ) + assertEquals("Final field writes belong after the loop",0,st.cfg0().loopDepth()); + return null; + }); + String[] sources={ + "struct C { val x=1; new C={->x=1; x=2;}; }; return new C().x;", + "struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i<n;i++) self.x*=2;}; }; return new C(arg).x;", + "struct C { val x=1; new C={int n -> for(int i=0;i<n;i++) ++x;}; }; return new C(arg).x;", + "struct C { val x=1.5; new C={->x=2.5;}; }; return new C().x;", + "struct C { val x=\"a\"; new C={int n -> for(int i=0;i<n;i++) x=\"b\";}; }; return new C(arg).x[0];", + "struct C { val x=new int[1]; new C={->x=new int[2]; x=new int[3];}; }; return new C().x#;" + }; + String[] results={"2","8","4","2.5","98","3"}; + for( int i=0; i<sources.length; i++ ) + assertEquals(results[i],Eval2.eval(new CodeGen(sources[i]).driver(CodeGen.Phase.TypeCheck),3)); + + String[] rejected={ + "struct C { val x=1; }; val c=new C(); c.x=2; return c.x;", + "struct C { new C={->val x=1; x=2;}; }; return new C();", + "struct C { val f={->1;}; new C={->f={->2;};}; }; return new C().f();" + }; + for( String src : rejected ) { + try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(src); } + catch( Parser.ParseException e ) { assertTrue(e.getMessage(),e.getMessage().startsWith("Cannot")); } + } + + var dir=java.nio.file.Path.of("build/objs"); + java.nio.file.Files.createDirectories(dir); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.smp"),"val buildFinal={int arg ->"+LOOP+"};"); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.c"),""" + extern long long buildFinal(long long); + int main(void) { return buildFinal(0)!=1 || buildFinal(3)!=15 || buildFinal(5)!=63; } + """); + TestC.run("build/objs","finalConstruction","",-1); + } +} diff --git a/chapter-incomplete-types/README.md b/chapter-incomplete-types/README.md index bfe0d44b..3ef144ed 100644 --- a/chapter-incomplete-types/README.md +++ b/chapter-incomplete-types/README.md @@ -7,8 +7,8 @@ English | [日本語](README.ja.md) The previous compiler solved for recursive types and calls, but its parser still needs types to choose e.g. float-vs-integer arithmetic instructions or -field tyoes. Forward references, or external references or even simple loop -structures can hide whats needed. +field types. Forward references, external references, or even simple loop +structures can hide what's needed. This chapter records the operations first and resolves their type-dependent parts later. Scalar types, numeric modes, conversions, guards, symbolic @@ -27,25 +27,25 @@ return add(new Later { x = 2.5; }); When parsing `p.x + 1`, the compiler knows the receiver's name (type `Later`), but not its fields! It makes a Load with a symbolic field offset `FldOff` and an `Add` whose numeric mode is still unset: might be an integer add or a -floatint point add. Later we parse `Later` which supplies the field and its +floating-point add. Later we parse `Later`, which supplies the field and its storage type. Type propagation then selects floating-point addition and converts the integer `1` operand. The result is some floating point math -`2,5 + (flt)1` which folds to `3.5`. +`2.5 + (flt)1`, which folds to `3.5`. Forward writes work the same way. Until a field is known, its Store carries whole memory - it aliases everything until we know what it aliases. Once resolved, the Store converts its value to the declared field type and updates that field's alias, which generally will result in a cascade of peepholes -sharpening the memory edges. The Store always keeps its control. Update -operators, e.g. ` x += y` work as syntax sugar over `x = x + y` (which might -need e.g. narrowing integer results when storing into a narrow `u8` field, or -not if storing into a `flt64` field). +sharpening the memory edges. Resolving the alias preserves the Store's control +input. Update operators such as `x += y` work like `x = x + y`, while evaluating +the destination only once. Storing into a `u8` field may narrow an integer +result; storing into an `f64` field may instead convert it to floating point. `FldOff` deserves more mention: it represents a named field reference (Load or -Store) to an unknown `TypeStruct`. In the above example, thats `p.x` : field -`x` from an unknown value of type `A{var x, ...}`. Since struct `A` can pick -up any more fields, and the field type itself is the unknown `var` type, -`FldOff` really knows very little about what's going on. Its more of a *shape* +Store) to an incomplete `TypeStruct`. In the example above, that's `p.x`: field +`x` of a `Later` whose fields are not yet declared. The struct can gain more +fields, and the field type itself is still unknown, so +`FldOff` really knows very little about what's going on. It's more of a *shape* placeholder, and awaits stronger types to peephole into a "real" field reference. @@ -54,7 +54,7 @@ reference. The parser always knows when it has *values* vs memory or control, but maybe not whether something is an int or a flt until much later. Instead it uses -`TypeScalar` for such this. `TypeScalar` contains integers, floats, object +`TypeScalar` for this. `TypeScalar` contains integers, floats, object pointers, and function pointers. These are classic *values* in the sense that they represent small concrete things that typically fit in a machine register. Values exclude control and memory. Scalar bottom says that a value's family is @@ -62,30 +62,31 @@ not known; its top represents an unreachable scalar result. A scalar union can still be tested for truth, even when its alternatives use different families; integers, floats, and pointers all carry a notion of zero. -A scalar Phi now meets its live inputs, it no longer carries a parser-supplied +A scalar Phi now meets its live inputs; it no longer carries a parser-supplied lower bound. An unfinished loop Phi starts at scalar bottom because its backedge is missing. Parameters retain their declared interface types, and -memory Phis retain their memory role. Loop, parameter, and recursive-return +memory Phis retain their memory role. Loop Phis, Parms, and recursive CallEnds still do widening. ## Record conversions and branch facts The parser ends up inserting `Convert` nodes where it used to directly convert -values. This includes widening integers to match float operators, narrowing -when storing into narrow integer fields, or widening when loading from them. -A bad load or store still produces bottom. Type checking errors at conversions -simply "pass the buck" to their broken input. +values. This includes converting integers to match float operators and narrowing +values stored into integer fields. Loads derive their value types from memory; +they no longer impose a parser-supplied bound. A bad load or store still produces +bottom. Type checking looks through a conversion with an already-broken input +so it can report the original error. -`Convert` records an known type destination, such as a local declaration or +`Convert` records a known destination type, such as a local declaration or function parameter. It waits for its input type to sharpen, and then peepholes -into a widening, truncation, permission check, or no operation. +into a widening, truncation, permission check, or no operation. `Guard` records a branch fact: zero/null or nonzero/non-null. Its value family comes from its input when known. The control edge keeps the fact on the path where it was proved. A guard output is the same runtime value as its input; it emits no instruction, just the sharpened type result. Guards are typically -used track not-null pointer tests across large areas. +used to track not-null pointer tests across large areas. Arithmetic nodes carry a `mode`: a 3-state enum tracking an unset, integer, or floating-point mode. Known inputs can settle it during peepholes. SCCP @@ -101,24 +102,24 @@ val foo = { A a -> a.b; }; struct A { B b; }; ``` -In this code, when parsing `foo` we do not know what `a` nor `a.b` will be, -other than there is a `b` field load on `a`, so `a` is a `TypeStruct` of `A`. +When parsing `foo`, we know that `a` points to an `A`, but not the fields of +`A` or the type of `a.b`. The load records that `A` must have a field named `b`. Forward shapes (`A`) remain *open* until their declaration, as in prior -chapters. When parsing `struct A` we find `B`, but `B` here is never declared... and +chapters. When parsing `struct A` we find `B`, but `B` here is never declared... and does not need to be. After parsing, the compiler *closes* all declarations as a group, including -types cycles (such as a `struct LinkedList` having a `next` field typed as a -`LinkedList`), and through function signatures. It then replaces old types -shapes in cached node types and stored declarations, (preserving pointer -permissions and nullability). This gives us a simple, clean *complete* version -of all mentioned types, no partially-made types from early in the parse remain. +type cycles (such as a `struct LinkedList` having a `next` field typed as a +`LinkedList`) and references through function signatures. It then replaces old +type shapes in cached node types and stored declarations, preserving pointer +permissions and nullability. Declared types now have their complete field sets. +An undeclared type such as `B` may remain opaque: this example only passes its +pointer around and does not need to allocate a `B` or access its fields. -We do not know if the program will type-check yet, but at least we know what -the types *are*. +Closing declarations does not itself prove the program will type-check. -At this point `FldOff` can being to peephole into a `ConFldOff` - moving -from a purely symbol field reference of an unknown type to a concrete +At this point `FldOff` can begin to peephole into a `ConFldOff` - moving +from a symbolic field reference of an incomplete type to a concrete field of a known type. Later, after structures get a *layout*, `ConFldOff` will become a concrete field offset, a number like `4` or `8`. diff --git a/chapter-interprocedural-sccp/README.ja.md b/chapter-interprocedural-sccp/README.ja.md index 4dec73c7..e9e57c6d 100644 --- a/chapter-interprocedural-sccp/README.ja.md +++ b/chapter-interprocedural-sccp/README.ja.md @@ -14,11 +14,11 @@ **Simple** では、ここでもほぼ何もせず、SCCP を手続き間で実行します。`CallNode` は `FunNode` を呼び、`ReturnNode` は `CallEndNode` に戻り、まったく通常の方法で型情報を渡します。ただし問題が1つあります。`Call` と `Fun`、および `Return` と `CallEnd` の間に辺がありません。そのような辺が [呼び出しグラフ](https://en.wikipedia.org/wiki/Call_graph) を構成します。 -通常の SCCP を拡張し、`Call` の関数ポインタ入力が変わり、新しい関数対象を得る時点を監視します。対象の数は有限なので、前の章で説明した *関数インデックス* で正確に表します。分割コンパイルでは、このコンパイル単位の外で呼ばれる無限の未知関数に対して、特別なインデックスを追加します。 +通常の SCCP を拡張し、`Call` の関数ポインタ入力が変わり、新しい関数対象を得る時点を監視します。対象の数は有限なので、前の章で説明した *関数インデックス* で表します。未知の対象には保守的な要約が必要です。後の分割コンパイルでは、オブジェクトファイルをまたいで関数の同一性も保ちます。 -`Call` で新しい *fidx* を見つけたら、`Call` と `Fun`、`Return` と `CallEnd` を *リンク* します。このリンクは、`Call` から `Fun`、そして `Call` の引数から `Fun` の `ParmNode` へ CFG 辺を追加したかのように動きます。`FunNode` は `RegionNode` を、`ParmNode` は `PhiNode` を拡張するため、同じ意味規則を使います。`Fun` は呼ばれると到達可能になり、`Parm` はすべての呼び出し引数をマージ(*meet*)します。同様に `CallEnd` は、すべての `Return` の *meet* を計算して、呼ばれたすべての関数の結果をマージします。 +`Call` で新しい *fidx* を見つけたら、`Call` と `Fun`、`Return` と `CallEnd` を *リンク* します。このリンクは、`Call` から `Fun` へ制御辺を、`Call` の引数から `Fun` の `ParmNode` へデータ辺を追加したかのように動きます。`FunNode` は `RegionNode` を、`ParmNode` は `PhiNode` を拡張するため、同じ意味規則を使います。`Fun` は呼ばれると到達可能になり、`Parm` はすべての呼び出し引数をマージ(*meet*)します。同様に `CallEnd` は、すべての `Return` の *meet* を計算して、呼ばれたすべての関数の結果をマージします。 -この *リンク* 段階はアルゴリズムの内部ループにいくつかの検査を加え、正確な呼び出しグラフも構築し、SCCP の手続き間実行を可能にします。進行中に新しい辺を少し発見する以外は、中核アルゴリズムは変わりません。不動点解で停止することが保証され、たとえば特定の呼び出しパラメータが、すべての呼び出し経路で定数、あるいは非 null と発見できることがあります。 +この *リンク* 段階はアルゴリズムの内部ループにいくつかの検査を加え、型伝播で発見した対象から保守的な呼び出しグラフを構築します。進行中に新しい辺を少し発見する以外は、中核アルゴリズムは変わりません。不動点解で停止することが保証され、たとえば特定の呼び出しパラメータが、すべての呼び出し経路で定数、あるいは非 null と発見できることがあります。 ## 再帰もループ diff --git a/chapter-interprocedural-sccp/README.md b/chapter-interprocedural-sccp/README.md index 7310de80..d17f937c 100644 --- a/chapter-interprocedural-sccp/README.md +++ b/chapter-interprocedural-sccp/README.md @@ -3,11 +3,11 @@ English | [日本語](README.ja.md) [Previous: SCCP Within a Function](../chapter-sccp/README.md) | -[Next: SSA Before Types Are Known](../chapter-incomplete-types/README.md) | +[Next: SSA Before Types Are Known](../chapter-incomplete-types/README.md) -The previous chapter runs SCCP with each function, and assumes very +The previous chapter runs SCCP within each function, and assumes very conservative typing across calls. This chapter lets the same worklist -discover call edges and propagate values between functions. +discover call edges and propagate values between functions. ## Discovering calls while solving types @@ -20,21 +20,21 @@ Such edges make up a [Call Graph](https://en.wikipedia.org/wiki/Call_graph). We extend the normal SCCP algorithm to observe when a `Call`s function pointer input changes - when it picks up new function targets. There are only a finite number so we represent them exactly with *function indices*, or *fidx* -internally, as described earlier chapters. (Separate compilation adds a -special index for the infinite unknown functions that are called outside this -compilation unit). +internally, as described in earlier chapters. Unknown targets still require +conservative summaries. Later, separate compilation must also preserve function +identities across object files. Once we observe a new *fidx* at a `Call`, we *link* the `Call` with the `Fun`, and the `Return` with the `CallEnd`. Such linkage acts exactly as-if we added -a CFG edge from the `Call` to the `Fun`, and the `Call`s arguments to the -`Fun`s `ParmNodes`. `FunNodes` extend `RegionNodes` (and `ParmNodes` extend +a CFG edge from the `Call` to the `Fun`, and data edges from the `Call`s arguments +to the `Fun`s `ParmNodes`. `FunNodes` extend `RegionNodes` (and `ParmNodes` extend `PhiNodes`), so this linkage uses the same semantics. `Fun`s become reachable when called; `Parm`s merge (*meet*) all calling arguments, and so on. Similarly, `CallEnd`s merge results from all called functions, computing the *meet* over all the `Return`s. This *link* step adds a few checks to our algorithm's inner loop, and also -builds a precise Call Graph which allows our SCCP to run interprocedurally. +builds a conservative Call Graph from the targets found by type propagation. Other than discovering a few new edges as the algorithm proceeds, the core algorithm is unchanged. We are guaranteed to terminate with a fixed point solution, and may discover e.g. certain call parameters are constants (or @@ -42,25 +42,26 @@ e.g. not-null) on all calling paths. ## Recursion is a loop in disguise -We use the notion of *widening* integer ranges at loop heads, see Chapter XXX. +We use *widening* of integer ranges at loop heads, as described in the +[previous chapter](../chapter-sccp/README.md#convergence-and-calls). This allows us to have a sharper notion of integers over `BOTTOM` without also -allowing the lattice to fall 2^32 times. The same issue arises during +allowing the range to grow one integer at a time. The same issue arises during recursive calls: an argument can flow from a Parm through arithmetic into a recursive call, then back to the same Parm. ```simple -val fact = { int x -> x ? 0 : f(x-1)+1; }; +val f = { int x -> x == 0 ? 0 : f(x-1)+1; }; ``` -Here if we pass in `f(999)` , parameter `x` will have the integer range -`[999]`. We'll then track the recursive call to `f(998)` via normal SCCP -and realize the parameter `x` has a range `[999-998]`. Lather, rinse, repeat -and we're at `[999-997]`. We need to *widen* integer parameters at `Parms` -not just loop-head `Phis`. +If the only outside call is `f(999)`, parameter `x` starts with the range +`[999,999]`. We'll then track the recursive call to `f(998)` via normal SCCP +and discover that `x` has range `[998,999]`. Lather, rinse, repeat, +and we're at `[997,999]`. We need to *widen* integer parameters at `Parms`, +not just loop-head `Phis`. The same issue arises during the recursive return: the first return will be -with `0` when `x` is zero, then we return `0+1` or the range `[0-1]` then range -`[0-2]`, etc. `CallEnds` also need to widen. +with `0` when `x` is zero, then we return `0+1` or the range `[0,1]`, then range +`[0,2]`, etc. `CallEnds` also need to widen. Loop Phis, Parms, and CallEnds get widening points, and since this same issue hits in 3 unrelated places, their comments all cross-reference each other. @@ -72,7 +73,7 @@ A `FunPtrNode` (function address) that survives outside a direct call may be called by code we cannot see. We have to assume the address escapes into unknown code, which later calls it. We preserve its unknown-caller input, since we cannot tell what that unknown caller will pass. Precise escape -analysis is introduced later exactly to inmprove this. +analysis is introduced later to improve this. ## Building and checking diff --git a/chapter-modules/Makefile b/chapter-modules/Makefile new file mode 100644 index 00000000..e89e1bd7 --- /dev/null +++ b/chapter-modules/Makefile @@ -0,0 +1,201 @@ +# +# Makefile expects to run at the project root, and not nested inside anywhere +# +# cd $DESK/Simple/chapterXX; make +# +# +# first time only - this is the only thing that hits the Interwebs +# +# make lib +# +# any time you want to nuke everything from space: +# +# make clean tags +# +# run the tests +# +# make tests + +####################################################### +# +# Boilerplate because make syntax isn't the best +# + +# Use bash for recipes +SHELL := /bin/bash + +# Remove target on error (no broken leftover files) +.DELETE_ON_ERROR: + +# Keep partial builds but not partial recipes; keeping them speeds up the next `make` +.NOTINTERMEDIATE: + +# for printing variable values +# usage: make print-VARIABLE +# > VARIABLE = value_of_variable +print-% : ; @echo $* = $($*) + +# literal space +space := $() $() + +# Decide OS-specific questions +# jar-file seperator +ifeq ($(OS),Windows_NT) + SEP = ; +else + SEP = : +endif +# Find a reasonable ctags. +CTAGS = $(shell which ctags) +# Hack for MacOS: /usr/bin/ctags is unfriendly, so look for ctags from brew +ifeq ($(UNAME),Darwin) + CTAGS = $(shell brew list ctags 2> /dev/null | grep bin/ctags) +endif + +# Fun Args to javac. Mostly limit to java8 source definitions, and fairly +# aggressive lint warnings. +JAVAC_ARGS = -g + +# Source code +SIMPLE := com/seaofnodes/simple +SRC := src/main/java +TST := src/test/java +CLZDIR:= build/classes +main_javas := $(wildcard $(SRC)/$(SIMPLE)/*java $(SRC)/$(SIMPLE)/*/*java $(SRC)/$(SIMPLE)/node/cpus/*/*java) +test_javas := $(wildcard $(TST)/$(SIMPLE)/*java $(TST)/$(SIMPLE)/*/*java) +main_classes := $(patsubst $(SRC)/%java,$(CLZDIR)/main/%class,$(main_javas)) +include $(firstword $(wildcard ../graph/graph.mk graph/graph.mk)) +include $(firstword $(wildcard ../print/print.mk print/print.mk)) +include $(firstword $(wildcard ../isa/isa.mk isa/isa.mk)) +test_classes := $(patsubst $(TST)/%java,$(CLZDIR)/test/%class,$(test_javas)) +test_cp := $(patsubst $(TST)/$(SIMPLE)/%.java,com.seaofnodes.simple.%,$(wildcard $(TST)/$(SIMPLE)/*Test.java)) +classes = $(main_classes) $(test_classes) +# All the libraries +libs = $(wildcard lib/*jar) +jars = $(subst $(space),$(SEP),$(libs)) + + +default_targets := $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag +# Optionally add ctags to the default target if a reasonable one was found. +ifneq ($(CTAGS),) +default_targets += tags +endif + +default: $(default_targets) + +####################################################### +# Compile the chapter and shared viewer sources together + +$(CLZDIR)/main/.mtag: $(main_javas) Makefile + @[ -d $(CLZDIR)/main ] || mkdir -p $(CLZDIR)/main + @# This crap is really just "javac", but with a beautiful error message. + @# The very very long list of java files is suppressed and counted. + @# The required output "Note: blah blah deprecated blah" is suppressed. + $(file >.argsM.txt,$(main_javas)) + @if [ ! -z "$(main_javas)" ] ; then \ + echo "compiling main because " $< " and " `wc -w < .argsM.txt` " more files" ; \ + if ! javac $(JAVAC_ARGS) -cp "$(CLZDIR)/main$(SEP)$(jars)" -sourcepath $(SRC) -d $(CLZDIR)/main $(main_javas) >& .out.txt ; then \ + cat .out.txt ; \ + exit 1; \ + fi ; \ + rm -rf .out.txt ; \ + fi + @touch $(CLZDIR)/main/.mtag + @rm -f .argsM.txt + +$(main_classes): $(CLZDIR)/main/.mtag + + + +####################################################### + +OODT := $(isa_test_javas) +$(CLZDIR)/test/.ttag: $(test_classes) $(test_javas) $(isa_test_javas) $(CLZDIR)/main/.mtag + @[ -d $(CLZDIR)/test ] || mkdir -p $(CLZDIR)/test + $(file >.argsT.txt,$(OODT)) + @if [ ! -z "$(OODT)" ] ; then \ + echo "compiling test because " $< " and " `wc -w < .argsT.txt` " more files" ; \ + if ! javac $(JAVAC_ARGS) -cp "$(CLZDIR)/test$(SEP)$(CLZDIR)/main$(SEP)$(jars)" -sourcepath $(TST) -d $(CLZDIR)/test $(OODT) >& .out.txt ; then \ + cat .out.txt ; \ + exit 1; \ + fi ; \ + rm -rf .out.txt ; \ + fi + @touch $(CLZDIR)/test/.ttag + @rm -f .argsT.txt + +# Collect just the out-of-date files +$(test_classes): $(CLZDIR)/test/%class: $(TST)/%java + $(eval OODT += $$<) + +####################################################### +# Base launch line for JVM tests +JVM=nice java -ea -cp "$(CLZDIR)/main${SEP}${jars}${SEP}$(CLZDIR)/test" + + +tests: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag + @echo "testing " $(test_cp) + @[ -d build/objs ] || mkdir -p build/objs + @$(JVM) org.junit.runner.JUnitCore $(test_cp) + @$(JVM) org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap + +fuzzer: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag + @echo "fuzzing " $(test_cp) + @$(JVM) org.junit.runner.JUnitCore com.seaofnodes.simple.FuzzerWrap + +# Build a Simple jar +release: build/release/simple.jar + +# Build a Simple jar +build/release/simple.jar: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag + @echo "jarring " $@ " because " $? + @[ -d $(dir $@) ] || mkdir -p $(dir $@) + @jar cf build/release/simple.jar -C $(CLZDIR)/main . -C $(CLZDIR)/test . -C $(SRC)/$(SIMPLE) . -C $(TST)/$(SIMPLE) . + + +.PHONY: clean +clean: + rm -rf build + rm -f TAGS + (find . -name "*~" -exec rm {} \; 2>/dev/null; exit 0) + +# Download libs from maven +lib: lib/junit-4.12.jar lib/hamcrest-core-1.3.jar + +# Jars +lib/junit-4.12.jar: + @[ -d lib ] || mkdir -p lib + @(cd lib; wget https://repo1.maven.org/maven2/junit/junit/4.12/junit-4.12.jar) + +lib/hamcrest-core-1.3.jar: + @[ -d lib ] || mkdir -p lib + @(cd lib; wget https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar) + +# Build emacs tags (part of a tasty emacs ide experience) +tags: $(main_javas) $(test_javas) + @rm -f TAGS + @$(CTAGS) -e --recurse=yes --extra=+q --fields=+fksaiS $(SRC) $(TST) + +spill-stats: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag + @[ -d build/objs ] || mkdir -p build/objs + @$(JVM) com.seaofnodes.simple.SpillStats + +.PHONY: spill-stats + +.PHONY: build +build: $(CLZDIR)/main/.mtag + +# The ABI shim gives standalone native tests the same entry contract as clients. +CPU_PORT := x86_64_v2 +ifeq ($(OS),Windows_NT) +CALL_CONV := win64 +NATIVE_ABI := ms_abi +else +CALL_CONV := SystemV +NATIVE_ABI := sysv_abi +endif +runtime_obj := build/release/$(CPU_PORT)_$(CALL_CONV)/simple_crt.o +$(runtime_obj): src/main/c/simple_crt.c Makefile + @mkdir -p $(dir $@) + gcc -c -O2 -DCALL_CONV=$(NATIVE_ABI) $< -o $@ +tests: $(runtime_obj) diff --git a/chapter-modules/README.ja.md b/chapter-modules/README.ja.md new file mode 100644 index 00000000..3765eeff --- /dev/null +++ b/chapter-modules/README.ja.md @@ -0,0 +1,91 @@ +# モジュールと遅延クラス初期化 + +[English](README.md) | 日本語 + +[前: エスケープ解析と未知の呼び出し元](../chapter-escape/README.ja.md) + +この章では複数のソースファイルを一度にコンパイルします。各ファイルは +一つのクラスと共有オブジェクトを定義し、トップレベルの文は隠れた +`<clinit>` 関数になります。最初の能動的な使用で初期化し、その後は +同じ結果を使います。クラスへの参照を保持するだけでは初期化しません。 + +## 必要なときだけ表を作る + +実行できる例は [`src/test/smp/lazy`](src/test/smp/lazy) にあります。 +`App.smp` は Main、Tables、Stats への参照を公開します。 +`App/Stats.smp` の `builds` は初期化回数を数えます。 +`App/Tables.smp` は次のように表を構築します。 + +```simple +Stats.builds++; +int[] _squares=new int[256]; +for(int i=0; i<256; i++) _squares[i]=i*i; +val square={u8 n -> _squares[n];}; +``` + +`App/Main.smp` が入口です。 + +```simple +return arg ? Tables.square(3)+Tables.square(4)+100*Stats.builds : Stats.builds; +``` + +引数が 0 なら結果は 0 で、Tables は初期化されません。引数が 1 なら +9 + 16 + 100 = 125 です。二度の呼び出しでも表は一度しか作りません。 +`_squares` は非公開のフィールドで、公開関数を通して読み出します。 +完全なファイル一覧と評価用 Java コードは [英語版](README.md) を参照してください。 + +## ファイルと解析の境界 + +`ParseAll` は未解決のクラス名や型から必要なソースを探します。 +ディレクトリ階層が名前空間になり、`App/Main` から `Tables` は兄弟の +`App/Tables` を指せます。親の初期化は子より先なので、親も解析します。 +相互参照する型はソースの発見後に閉じます。 + +`CompUnit` はソース、クラス型、関数をまとめます。各単位には +`StartCU` と `StopCU` があり、全体の Start/Stop の下で解析します。 +公開関数には未知の呼び出し元が存在します。非公開の関数は引き続き +エスケープ解析によって特殊化できます。フィールド名の先頭の `_` は +非公開を表します。ただし、現実装はこれを公開インターフェースの選択に使うだけで、 +兄弟クラスからの直接アクセスをまだ拒否しません。このアクセス検査は未実装です。 + +標準ライブラリ `sys` もソースから解析します。ローカルの `sys` が +なければ `src/main/smp` を使います。ネイティブの `"C"` 宣言は従来通り +ネイティブリンカーで解決します。 + +## 初期化の順序と循環 + +クラスのフィールドの読み書き、メソッド呼び出し、インスタンス生成は +初期化を必要とします。パーサーは `CLInit` を挿入し、最適化中は制御と +メモリ効果を持つ不透明なノードとして残します。実行後は隠れた `$init` +フラグが真なので、通常のメモリ最適化で重複した検査を除去できます。 + +型やクラスへの参照の循環は合法です。しかし A の初期化が B を使い、 +B の初期化が A を使う場合、先に完了できるクラスがありません。 +`ClassInitDependencies` は最適化後の呼び出しと `CLInit` をたどり、 +複数のクラス初期化を含む循環をソース位置付きで報告します。 +通常の再帰や同じ初期化関数内の再帰は許されます。 + +検査後、`Lower` はフラグ検査と未初期化時の呼び出しに展開します。 +親を先に初期化し、フラグは初期化関数の入口で真にします。同じクラスのヘルパーから +再び使えるようにするためです。他クラスを経由して未完成のクラスへ戻る循環は拒否します。 +初期化関数 A が B の初期化を完了させてから再開することは可能です。 + +## コード生成とビルド + +この章では必要なソースをすべて同時にコンパイルします。`FinishCalls` +が解析用の呼び出し辺を切り離し、`StaticData` がクラスの共有領域と +定数フィールドを配置します。ELF は発見したソースのうち生きているコードを含みますが、 +理想 IR のシリアライズ情報は含みません。 + +リポジトリのルートで次を実行します。 + +```sh +make tests CHAPTERS=chapter-modules +``` + +このディレクトリでは初回に `make lib`、続いて `make tests` を使えます。 +`make view` は共有グラフビューアーを起動します。 + +次の章ではライブラリのソースがなくてもコンパイルできるように、型と +理想 IR の保存、インポート時の識別子変換、インポート先を含む初期化検査 +を追加します。ライブラリ構築と最適化の調整は付録として扱います。 diff --git a/chapter-modules/README.md b/chapter-modules/README.md new file mode 100644 index 00000000..f0fac494 --- /dev/null +++ b/chapter-modules/README.md @@ -0,0 +1,271 @@ +# Modules and Lazy Class Initialization + +English | [日本語](README.ja.md) + +[Previous: Escape Analysis and Unknown Callers](../chapter-escape/README.md) + + +The compiler can now discover several source files and compile them together. +Each file defines a class with one shared class object; files can nest in +directories forming a module structure. + +The shared class object is a normal object with fields holding function pointers +or other state. The top-level code in the file forms the `<clinit>` +function, which runs once only on the first active use of the class - i.e., +lazy class initialization. An initializer can trigger another class's +initialization and wait for it to finish. It cannot observe that other class +half-initialized. Cycles between class initializers are not allowed. + +This chapter adds file and class semantics to the escape analysis we already +have. All required Simple source is available in one compilation. + + +## Example: Lazy class initialization, or Pay-As-You-Go + +Suppose an application sometimes needs a `Table` but not often; many runs may +never need it. Putting its construction in the application entry would make +every run pay (the eager-init model). Putting it in a getter would need a test +and cached storage. A class initializer provides `Table` lazily, on demand, +and once for all users of the class. This is similar to the Java class init +model except nothing ever sees a class mid-initialization, except the class +itself. + +The runnable example is in [`src/test/smp/lazy`](src/test/smp/lazy). +Its directory tree also defines the namespace: + +```text +App.smp +App/Main.smp +App/Stats.smp +App/Tables.smp +``` + +`App.smp` names its children: + +```simple +val Main=Main; +val Tables=Tables; +val Stats=Stats; +``` + +These are class references being stored into normal fields, to give easy access +to nested classes. Merely storing the reference does not trigger the class +initializer. + +`App/Stats.smp` supplies a visible count: + +```simple +int builds=0; // Static global counter +``` + +`App/Tables.smp` builds the table and exposes a function that reads it; the +array is marked private and its public function is the intended interface +(see the current access-check limitation below): + +```simple +Stats.builds++; // Number of table builds... only ever zero or 1! + +// Build a private table of squares +int[] _squares = new int[256]; +for(int i=0; i<256; i++) _squares[i]=i*i; + +// Public accessor to get a square +val square = { u8 n -> _squares[n];}; +``` + +`App/Main.smp` is the main entry: + +```simple +return arg ? Tables.square(3)+Tables.square(4)+100*Stats.builds : Stats.builds; +``` + +With `arg == 0`, the result is 0 and `Tables` never initializes. With `arg == 1`, +the result is 125: the two lookups produce 9 and 16, and the table is built once. +From this chapter directory, the test driver can evaluate it: + +```sh +java -ea -cp "build/classes/main:build/classes/test:lib/*" \ + com.seaofnodes.simple.Simple --root src/test/smp/lazy --eval App/Main.smp 1 +``` + +On Windows, use `;` instead of `:` between classpath entries. + +The evaluator can also run the example directly after type checking: + +```java +CodeGen code=new CodeGen("src/test/smp/lazy", "build/objs", "App/Main", + null, 123, true, TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); +Eval2.eval(code,0); // "0" +Eval2.eval(code,1); // "125"; a fresh evaluation starts a fresh program +``` + +`BoundaryTest.testLazyTables` checks these results. `ClassInitDependenciesTest` +also runs native code that calls a class repeatedly in one process, checking +both the cold and already-initialized paths. + +### Lazy initialization details + +The implementation tracks a hidden `$init` variable in every class. It changes +from `false` to `true` at initializer entry, allowing the class's own helpers +to use it while initialization is in progress. The separate cycle check prevents +another class's initializer from reaching back into that unfinished class. +A check before each active use calls the initializer if needed. The optimizer +tracks the flag as a normal alias, distinguishes `false`, `true`, and `bool`, +and can sometimes remove the test and call. + + +## Name lookup + +If a name misses in the file scope, the parser looks for a matching Simple +source file under the module root. It searches the current class's children, +then enclosing scopes. For example, in `App.smp`, the declaration +`val Main=Main;` defines a normal field, but the right-hand `Main` is unresolved. +The compiler finds `App/Main.smp`, which defines the referenced class. + +`ParseAll` starts with the requested file and follows unresolved class names +and types to source files. From `App/Main`, `Tables` can name the sibling +`App/Tables`; a longer name walks down the class tree. Long dotted paths are +also allowed: `App.Tables`. The parser also reads parents, since their +initialization must precede their children. Cyclic source references are +allowed: the compiler closes recursive types after discovery. + +`CompUnit` ties a source file, class type, and function graph together. They +form a tree that mirrors the source directories and tracks which files need +loading and parsing. Each `CompUnit` has a `StartCU` and `StopCU`, beneath +the whole program's `Start` and `Stop`. + +### Private + +A leading underscore marks a field or class as private. The intended rule is +that private fields are visible only inside their declaring class, not to +siblings or parents. A private enclosing class also keeps its children out of +the public interface. + +The current compiler uses these names to choose the public interface and seed +escape analysis, but does not yet enforce the source access rule. For example, +a sibling can currently read `A._secret`. Rejecting that access is still needed; +the underscore alone is not an enforced boundary in this snapshot. + +Like in other languages, private fields allow you to hide implementation +details. They can be exposed via the normal getter/setter route or any other +way that makes sense. Escape analysis can track private fields more precisely +when all uses stay in known code, helping the usual optimizations. + +### Other names + +The bundled `sys` library uses the same source mechanism. It is a normal module +in all ways except that lookup falls back to the bundled `src/main/smp` when +no local `sys` is found. Native `"C"` declarations still name functions +or data resolved by the native linker. + + +## Active use and initialization order + +Reading or writing a class field, calling its method, or constructing an +instance needs the class initialized; a class reference alone does not. The +parser inserts `CLInitNode` at these active uses, and uses the usual collection +of peepholes to optimize these away. E.g. a forward reference *might* be a +class reference, so a `CLInit` is conservatively inserted. Later the fref +resolves to a normal instance... so the `CLInit` will peephole away. +`CLInit` has peeps for removing repeated checks; for telling that the check +must always be `true` or `false`, etc. + + +Parents initialize before children. In the example, `App` initializes before +`App.Main` and `App.Tables`. Its passive references to the children do not run +them while `App` is still being initialized. + +The compiler keeps `CLInit` opaque through optimization. It carries control +and whole-memory effects, plus the fact that this class's hidden `$init` flag +is `true` afterward. At the `Lower` pass, the `CLInit` explodes into a true +CFG diamond guarding the `<clinit>` call. At this point we end up with a real +runtime guard check, i.e. real overhead. Removing redundant checks reduces +that cost; the first active use may still need a runtime check. + + +## Reject cycles that would observe unfinished classes + +An initializer can use a different class after that class finishes initializing. +For example, `A` can start, initialize `B`, then resume. A cycle would instead +ask one of them to use the other before it finishes. + +Passive references do not create initialization dependencies. These two sibling +files can refer to each other: + +```simple +// A.smp +val b = B; +// B.smp +val a = A; +``` + +Reading each other's fields during initialization does create a cycle: + +```simple +// A.smp +int value = B.value; +// B.smp +int value = A.value; +``` + +Parents add another dependency: a parent must finish before its child starts. +A parent initializer therefore cannot actively use its child. It can keep a +passive reference, or define a helper that uses the child when called after +the parent finishes. Calling that helper during parent initialization would +still create a cycle. + +The resulting dependency graph is a DAG, but initialization remains lazy. +Execution respects the dependencies of each active use; classes that are never +actively used need not initialize. + + +After optimization and during `TypeCheck`, `ClassInitDependencies` follows +surviving `CLInit` markers and calls. It checks strongly connected components +and reports cycles involving more than one class initializer. Ordinary +recursion and recursive helpers within a single initializer are allowed. Since +this is after SCCP, eliminated code cannot make a cycle. Surviving calls are +checked conservatively, even if a more powerful analysis could prove them dead. + +After checking, `Lower` expands each remaining marker into a flag test and a +call on the cold path. Parent initialization comes first. The initializer itself +sets the flag. Generated calls have conservative memory effects and do not +reopen SCCP's analysis of the optimized bodies. + + +## Emitting code and data + +After cleanup by `FinishCalls`, the live code and data from the discovered +sources are emitted in one ELF file. Final constant fields are emitted as +static data in the ELF using the helper `StaticData`; commonly this includes +static class fields holding function pointers, but any constant data can be +stored here. These fields then do not need any `<clinit>` code. + +Mutable field initialization remains in `<clinit>`, even if the initial value +is a constant. A final binding whose value is computed at runtime also needs +initializer code. For example, in `Theme.smp`: + +```simple +{int} read_setting = "C"; // Supplied by the native application +val scale = read_setting(); // Final binding; value only known at runtime +``` + + +## Build and review + +From the repository root: + +```sh +make tests CHAPTERS=chapter-modules +``` + +Or, in this directory, `make lib` once for JUnit, then `make tests`. The suite +covers source discovery, initialization cycles and parent order, native cold/hot +calls, shared class storage, constructor reads and loops, private callbacks, +and x86/ARM/RISC-V code generation. `make view` starts the shared graph viewer. + +The next chapter will keep this language behavior while allowing a client to +compile without the library's sources. That requires saving types and ideal IR, +remapping identities on import, and extending the same initialization checks +across imported bodies. Library construction and optimizer tuning remain +appendix topics in the [approved split](../docs/chapter-split.md). diff --git a/chapter-modules/pom.xml b/chapter-modules/pom.xml new file mode 100644 index 00000000..ac516e0b --- /dev/null +++ b/chapter-modules/pom.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.seaofnodes</groupId> + <artifactId>simple</artifactId> + <version>1.0</version> + </parent> + <artifactId>chapter-modules</artifactId> + <packaging>jar</packaging> + <name>Modules and Lazy Class Initialization</name> + <properties> + <simple.graph.directory>${project.basedir}/../graph</simple.graph.directory> + <simple.print.directory>${project.basedir}/../print</simple.print.directory> + </properties> + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>shared-isa-sources</id> + <phase>generate-sources</phase> + <goals><goal>add-source</goal></goals> + <configuration> + <sources> + <source>${project.basedir}/../isa/src/main/java</source> + </sources> + </configuration> + </execution> + <execution> + <id>shared-isa-test-sources</id> + <phase>generate-test-sources</phase> + <goals><goal>add-test-source</goal></goals> + <configuration><sources> + <source>${project.basedir}/../isa/src/test-support/java</source> + </sources></configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/chapter-modules/src/main/c/simple_crt.c b/chapter-modules/src/main/c/simple_crt.c new file mode 100644 index 00000000..8656a3cc --- /dev/null +++ b/chapter-modules/src/main/c/simple_crt.c @@ -0,0 +1,61 @@ +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> + +// errno may be thread-local or a macro calling a platform accessor. +int32_t simple_errno(void) { return errno; } + +// Native process arguments converted to ordinary Simple array layouts. +// The outer array is mutable; every contained byte array is published as +// immutable and non-null to Simple code. +typedef struct SimpleBytes { + uint32_t len; + unsigned char bytes[]; +} SimpleBytes; + +typedef struct SimpleArgs { + uint32_t len; + uint32_t pad; + SimpleBytes *args[]; +} SimpleArgs; + +__attribute__((CALL_CONV)) +extern int64_t simple_main(SimpleArgs *args); + +static SimpleArgs *simple_args(int argc, char **argv) { + if (argc < 0 || (uint64_t)argc > UINT32_MAX) + return NULL; + + SimpleArgs *args = calloc(1, sizeof(*args) + (size_t)argc * sizeof(args->args[0])); + if (args == NULL) + return NULL; + args->len = (uint32_t)argc; + + for (int i = 0; i < argc; i++) { + size_t len = strlen(argv[i]); + if (len > UINT32_MAX) { + free(args); + return NULL; + } + SimpleBytes *str = malloc(sizeof(*str) + len); + if (str == NULL) { + for (int j = 0; j < i; j++) free(args->args[j]); + free(args); + return NULL; + } + str->len = (uint32_t)len; + memcpy(str->bytes, argv[i], len); + args->args[i] = str; + } + return args; +} + +int main(int argc, char **argv) { + SimpleArgs *args = simple_args(argc, argv); + if (args == NULL) + return 1; + // The language-level hidden receiver is not part of the emitted ABI for + // free functions; source argument zero occupies native argument zero. + return (int)simple_main(args); +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/IterPeeps.java b/chapter-modules/src/main/java/com/seaofnodes/simple/IterPeeps.java new file mode 100644 index 00000000..e65ef6d9 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/IterPeeps.java @@ -0,0 +1,349 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Opto; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.node.*; +import java.util.Arrays; +import java.util.BitSet; +import java.util.IdentityHashMap; +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. It's 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 { + + public final WorkList<Node> _work; + final WorkList<CallEndNode> _workInline; + private int _cnt; + + public IterPeeps( long seed ) { + _work = new WorkList<>(seed); + _workInline = new WorkList<>(seed); + } + + @SuppressWarnings("unchecked") + public <N extends Node> N add( N n ) { return (N)_work.push(n); } + + public void addAll( Ary<Node> ary ) { _work.addAll(ary); } + + /** + * Iterate peepholes and inlining to a fixed point + */ + public void iterate( CodeGen code ) { + boolean didInline = false; + Ary<Node> defer = new Ary<>(Node.class); + while( true ) { + // Clean up everything that does not grow the code + iteratePeeps(code); + + // Pick an inline candidate, no real heuristic, first come, first served + boolean inlined = false; + CallEndNode cend; + while( (cend=_workInline.pop()) != null) { + if( cend.isDead() ) continue; + byte inline = cend.maybeInline(); + if( inline == -1 ) defer.add(cend); + if( inline > 0 ) { + inlined = true; + break; + } + } + // Inlined, run peeps until clean again + if( inlined ) { didInline = true; continue; } + // No more candidates, check the defer list + if( !didInline ) break; // No more progress, so all the "maybe inline after cleanup" do not progress + // Some inlining happened, retry all the "try again after cleanup" calls + didInline = false; + code.addAll(defer); + defer.clear(); + } + } + + // Run all the code-reduction and type-lifting peeps as possible + private void iteratePeeps( CodeGen code ) { + assert !CodeGen.expensiveAssert(0) || (progressOnList(code, _work) && schedulableUses(code)); + + Node n; + while( (n=_work.pop()) != null ) { + if( n.isDead() ) continue; + _cnt++; // Useful for debugging, searching which peephole broke things + var obs = code._obs; // Collect peephole neighbors for graph display + if( obs != null ) obs.before(n); + Node x = n.peepholeOpt(); + if( n instanceof CallEndNode cend ) + _workInline.push(cend); + if( x != null ) { + assert !x.isDead(); // Peepholes return alive answers + // 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); + // A self-returning peephole can have rewritten its input + // edges. The new defs gained a user and may have + // backwards, user-sensitive peepholes of their own. + for( Node z : x._inputs ) _work.push(z); + // 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); + for( Node z : x._outputs ) _work.push(z); + n.subsume(x); + } + } + // If there are distant neighbors, move to worklist + n.moveDepsToWorklist(); + // Very expensive assert. + assert !CodeGen.expensiveAssert(_cnt) || (progressOnList(code, _work) && schedulableUses(code)); + } + boolean unused = n.isUnused(); + if( unused ) { + assert !(n instanceof StopNode); // StopNodes can die if all code in the compunit dies + n.kill(); // Just plain dead + } + if( obs != null ) obs.after(n, x!=null ? x : unused ? n : null, true); + } + assert !CodeGen.expensiveAssert(0) || schedulableUses(code); + + } + + // 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. + // Pessimistic solver assert + public static boolean progressOnList(CodeGen code, WorkList<Node> list ) { + code._midAssert = true; + Node changed = code._stop.walk( n -> { + // Ignore most in-progress things + if( n.iskeep() ) return null; + // Types must be forwards, even if on the worklist. + Type nval = n.compute(); + assert nval.isa(n._type) : "Non-monotonic peep: "+n+"#"+n._nid+" old="+n._type+" new="+nval+" inputs="+inputTypes(n)+" peep="+n; + if( list.on(n) ) + return null; // On worklist is ok! + if( n instanceof CallEndNode cend ) { + if( code._iter._workInline.on(cend) ) + return null; // On inline worklist is ok! + assert cend.maybeInline() <= 0 : "Inline fired and not on worklist, CallEndNode#"+cend._nid; + } + // Off list: must be live, in GVN table, and no peeps. + assert n.nOuts() > 0 : "Unused live node: "+n; + assert code._gvn.get(n)==n : "Not in GVN table: "+n; + Node m = n.peepholeOpt(); + assert m==null : "Peep fired and not on worklist, "+n.getClass().getSimpleName()+"#"+n._nid+" -> "+m; + return m; + }); + code._midAssert = false; + return changed==null; + } + + private static String inputTypes(Node n) { + StringBuilder sb = new StringBuilder("["); + for( Node in : n._inputs ) + sb.append(in==null ? "null" : in.getClass().getSimpleName()+"#"+in._nid+"="+in+":"+in._type+":keep="+in.iskeep()).append(','); + return sb.append(']').toString(); + } + + // GCM assumes every movable node can be placed no earlier than its inputs + // and no later than the LCA of its uses. Catch graphs where a use escapes + // above an input-defined branch before the later scheduling pass walks off + // the top of the idom tree. + private static final IdentityHashMap<Node,CFGNode> earlyCache = new IdentityHashMap<>(); + private static final BitSet VISIT = new BitSet(); + public static boolean schedulableUses(CodeGen code) { + earlyCache.clear(); + Node bad = code._stop.walk( n -> { + if( n.iskeep() || n.isDead() || n.isConst() || + n instanceof CFGNode || n instanceof PhiNode || n instanceof ProjNode ) + return null; + assert VISIT.isEmpty(); + CFGNode early = CFGNode.earlyCFG(n,earlyCache,VISIT); + VISIT.clear(); + if( early == null ) return null; + CFGNode lca = null; + for( Node use : n._outputs ) { + CFGNode ublk = useBlock(n,use); + if( ublk != null ) + lca = ublk.domLCA(lca,null); + } + if( lca == null ) return null; + if( !early.sameFun(lca) ) return null; + assert early.dominates(lca,null) : badSchedule(n,early,lca); + return null; + }); + return bad == null; + } + + private static CFGNode useBlock(Node n, Node use) { + if( use == null ) + return null; + if( use instanceof PhiNode phi ) { + CFGNode found = null; + for( int i=1; i<phi.nIns(); i++ ) + if( phi.in(i)==n ) { + if( i >= phi.region().nIns() ) + return null; + found = phi.region().cfg(i).domLCA(found,null); + } + return found; + } + return CFGNode.safeCFG(use); + } + + private static String badSchedule(Node n, CFGNode early, CFGNode lca) { + StringBuilder sb = new StringBuilder("Unschedulable data node ") + .append(n.getClass().getSimpleName()).append('#').append(n._nid) + .append(" early=").append(cfg(early)) + .append(" use-lca=").append(cfg(lca)) + .append(" node=").append(n).append("\ninputs:"); + for( int i=0; i<n.nIns(); i++ ) { + Node in = n.in(i); + sb.append("\n in").append(i).append(": ").append(node(in)) + .append(" cfg=").append(cfg(CFGNode.safeCFG(in))); + } + sb.append("\noutputs:"); + for( Node use : n._outputs ) + if( use != null ) + sb.append("\n use ").append(node(use)) + .append(" block=").append(cfg(useBlock(n,use))); + return sb.toString(); + } + + private static String node(Node n) { + return n==null ? "null" : n.getClass().getSimpleName()+"#"+n._nid+" "+n; + } + + private static String cfg(CFGNode cfg) { + return cfg==null ? "null" : cfg.getClass().getSimpleName()+"#"+cfg._nid+" "+cfg; + } + + /** + * Classic WorkList, with a fast add/remove, dup removal, random pull. + * The Node's nid is used to check membership in the worklist. + */ + @SuppressWarnings("unchecked") + 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; + + public 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); + } + public void addAll( E[] es ) { + for( E n : es ) + push(n); + } + + + /** + * True if Node is on the WorkList + */ + public boolean on( E x ) { return _on.get(x._nid); } + + /** + * Removes a random Node from the WorkList; null if WorkList is empty + */ + public 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 boolean isEmpty() { return _len==0; } + //Node[] asAry() { return Arrays.copyOf(_es,_len); } + //public void clear() { + // _len = 0; + // _on.clear(); + // _R.setSeed(_seed); + // _totalWork = 0; + //} + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/Parser.java b/chapter-modules/src/main/java/com/seaofnodes/simple/Parser.java new file mode 100644 index 00000000..031d36c6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/Parser.java @@ -0,0 +1,3157 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.node.ScopeNode.Kind; + +import static com.seaofnodes.simple.util.Utils.TODO; + +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 { + + // Compile driver + public final CodeGen _code; + + // The Lexer. Thin wrapper over a byte[] buffer with a cursor. + private Lexer _lexer; + + // Class prefix string; TODO: use something short: "%" + public static final String CLZ = "class:"; + private static final String clzPrefix = CLZ; + public static String addClzPrefix( String x ) { + assert !x.startsWith(clzPrefix); + return (clzPrefix+x).intern(); + } + public static boolean startsClzPrefix( String s ) { + return s.startsWith(clzPrefix); + } + public static boolean isPublicClassName( String name ) { + if( startsClzPrefix(name) ) name = name.substring(clzPrefix.length()); + return !name.isEmpty() && name.charAt(0)!='_' && name.indexOf("._")==-1; + } + + // Source file for compilation + public CompUnit _ref; + + // Current classname being parsed + private String _nestedType; + /** + * 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 _breakScope; // Merge all the while-breaks here + ScopeNode _continueScope; // Merge all the while-continues here + ScopeNode _returnScope; // Merge all the function exits here + Ary<FRefNode> _frefs; // Forward refs created during this parse + TypeStruct _ctorOpenStruct; // Open struct being initialized by an allocation constructor block. + // Constructor exit types survive scope teardown until all fields are known. + // A null entry denotes a constructor with no returning path. + private ArrayList<IdentityHashMap<Var,Type>> _ctorExits; + + // Mapping from a type name to a Type. The string name matches + // `type.str()` call. + public static final HashMap<String, Type> TYPES = new HashMap<>(); + public static final HashMap<String, Type> INIT_TYPES = + 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 + }}; + + + public Parser(CodeGen code ) { + _code = code; + } + + @Override + public String toString() { return _lexer==null ? "" : _lexer.toString(); } + + // Debugging utility to find a Node by index + public Node f(int nid) { return _code.f(nid); } + + // Read and set CTRL + private Node ctrl() { return _scope.ctrl(); } + private <N extends Node> N ctrl(N n) { return _scope.ctrl(n); } + // Read and set Memory + private Node mem() { return _scope.mem(1); } + private void mem(Node mem) { _scope.mem(mem); } + // Structural label for precise memory Phis. The '$' prefix cannot be + // referenced in Simple source. + public static String memName(int alias) { return ("$"+alias).intern(); } + + // Ordinary parsing carries one whole-memory value. Alias partitioning is + // graph optimization, not parser state. + private void storeMem( Node st, int alias, Node prior ) { + mem(alias==1 ? st : mergeAlias(prior,alias,st)); + } + + // A semantic graph partition representing the complete memory after a + // precise alias update. ScopeNode treats this as one opaque value. + private Node mergeAlias(Node bulk, int alias, Node precise) { + assert bulk != null : "Missing bulk memory for alias #"+alias; + MemMergeNode merge = new MemMergeNode(null,bulk); + merge.alias(alias,precise); + return peep(merge); + } + + + public Ary<FRefNode> parse( CompUnit ref ) { + assert _scope == null && _breakScope == null && _continueScope == null && _returnScope == null; + assert _frefs == null; + _lexer = new Lexer(ref); + _frefs = new Ary<>(FRefNode.class); + // Starting Scope has control, memory, initial arguments + _scope = new ScopeNode(); + _scope.define(ScopeNode.CTRL, Type.CONTROL , false, null, _lexer); + _scope.define(ScopeNode.MEM0, TypeMem.BOT , false, null, _lexer); + // Track active scopes for Graph display + _xScopes.push(_scope); + ctrl(_code.XCTRL); + mem(con(TypeMem.BOT)); + + // File-level struct declaration + _ref = ref; + _nestedType = ref._cname; + String typeName = addClzPrefix(ref._cname); + + ReturnNode clzret = parseStruct( true, typeName ); + + if( !_lexer.isEOF() ) throw _errorSyntax("unexpected"); + + // All still-unresolved forward refs + // should immediately resolve to a file-based name which is a Simple + // class type. + Ary<FRefNode> frefs = new Ary<>(FRefNode.class); + for( FRefNode fref : _frefs ) { + if( fref.nIns() != 1 && fref._con == FRefNode.FREF_TYPE ) continue; + CompUnit cu = ParseAll.findCompUnitOrThrow(_code,_ref,fref._name); + fref._con = TypeMemPtr.make((byte)2,TypeStruct.make(addClzPrefix(cu._cname),true),true); + frefs.push(fref); + } + + // The class being published + ref._clz = (TypeStruct)TYPES.get(typeName); + + // Clean up and reset + _xScopes.pop(); + _scope.kill(); + _scope = null; + _frefs = null; + + return frefs; + } + + + /** + * Parses a function body, assuming the header is parsed. + */ + private ReturnNode parseFunctionBody( TypeFunPtr sig, Lexer loc, String[] ids) { + return parseFunctionBody(sig, loc, null, ids); + } + + private ReturnNode parseFunctionBody( TypeFunPtr sig, Lexer loc, java.util.BitSet finals, String[] ids) { + // Record & restore the existing scope vars. The function parse might + // update fields, so when the parse is done reset back to the pre-parse + // state. + Node[] preParse = _scope.save(); + + // Parse function body normally + ReturnNode ret = _parseFunctionBody(null, sig,loc,finals,ids); + + // Reset all fields to pre-parse days + _scope.restore(preParse); + return ret; + } + + + // Parse a function body + + // Stack the {break,continue,return} scopes and a new lexical scope. + // Build a FunNode and parameters, extending scope by parm names. + // Parse the function body + // - while( not end-of-function ) + // - - parseStatement + // - - If isInit, also incrementally extend the self-type. + // If isInit + // - upgrade and close the self-type + // - Gather fields and store values + // - <init> returns self, not last + // Unstack the {break,continue,return} scopes. + + private ReturnNode _parseFunctionBody( String funName, TypeFunPtr sig, Lexer loc, String[] ids) { + return _parseFunctionBody(funName, sig, loc, null, ids); + } + + private ReturnNode _parseFunctionBody( String funName, TypeFunPtr sig, Lexer loc, java.util.BitSet finals, String[] ids) { + Constructor outerCtor=_pendingCtor; + _pendingCtor=null; + boolean isInit = FunNode.isInit(funName); + boolean isCtor = isInit || isExplicitConstructorName(funName); + String typeName = isCtor ? ((TypeMemPtr)sig._sig[0])._obj._name : null; + + // Stack parser state on the local Java stack, and unstack it later + _scope.push(new Kind.Func(funName,isCtor && !funName.endsWith(".<clinit>"))); + ScopeNode breakScope = _breakScope; _breakScope = null; + ScopeNode continueScope = _continueScope; _continueScope = null; + ScopeNode returnScope = _returnScope; _returnScope = null; + + int oldUID = _code.UID(); // Used to approximate function size + FunNode fun = (FunNode)peep(new FunNode(loc(),sig, funName, _ref, null, _ref._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,ConstantNode.seed(TypeRPC.BOT).peephole()).peephole(); + + // 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.ctrl(fun); // Scope control from function + // Private mem alias tracking per function + Node defaultMem = new ProjNode(_ref._start,1,ScopeNode.MEM0).peephole(); + Node privMem = new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,defaultMem).peephole(); + mem(privMem); + // All args, "as-if" called externally + for( int i=0; i<ids.length; i++ ) { + Type t = sig.arg(i); + // Args take a default input if called from Start/external-world + _scope.define(ids[i], t, (i==0 && ids[0]=="self") || (finals!=null && finals.get(i)), new ParmNode(ids[i],i+2,t,fun,ConstantNode.seed(t).peephole()).peephole(), loc); + } + + if( isCtor && !isInit ) { + int lex=_scope.klast()._lexSize; + TypeStruct shape=(TypeStruct)TYPES.get(typeName); + for( Field fld : shape._fields ) { + int idx=fieldVarIdxBefore(lex,fld._fname); + if( idx<0 ) continue; + Var v=_scope.var(idx); + if( v._fref || v._extern!=0 || v._uninit || _scope.in(v)._type.isConstant() ) continue; + Node value=convertExpr( + new LoadNode(loc,fld._fname,fld._alias,ctrl(),fun.parm(3),fun.parm(2),off(shape,fld._fname)).peephole(),v.type()); + _scope.update(v,value); + } + } + + // The initializer owns its run-once flag store. Parent initialization + // is a structural requirement, executed before this body by CLInit lowering. + if( fun.isClz() ) { + Node self = fun.parm(2); + Field flag = ((TypeMemPtr)self._type)._obj.field("$init"); + Node prior = mem().keep(); + Node st = peep(new StoreNode(loc,"$init",flag._alias,flag._t,ctrl(),prior,self, + fldoff(self,"$init"),con(1),false)); + storeMem(st,flag._alias,prior); + prior.unkeep(); + } + + // Parse the function body. + Node last=_code.ZERO; // Last statement as the default result + + int nvar = 0; + while (!peek('}') && !_lexer.isEOF()) { + // Parse a statement; record last statement as default return + last = _code.add(parseStatement()); + // For <init> and <clinit>, local vars are really struct fields and + // get exposed as discovered. This allows late local functions to + // use early declared fields. + if( isInit ) + nvar = updateSelfAsFieldsDiscovered( typeName, nvar ); + } + + if( isInit ) finishConstructor(); + _pendingCtor=outerCtor; + + ParmNode self = fun.parm(2); + // Last expression is the return + if( ctrl()._type==Type.CONTROL ) { + // Constructors get special return types + if( fun.isClz() ) { + assert last!=null; + } else if( isCtor ) { + last = fun.parm(3); + } + addReturn(last); + // exit path is now dead (nothing live after a "return") + } + if( isCtor ) + upgradeSelfTypeAndStoreFields( typeName, fun.isClz(), self, fun.parm(3), isInit ); + + // Build a return from the _returnScope. + // Can be no returns for never-exit functions + Node rctl = _returnScope==null ? _code.XCTRL : _returnScope.ctrl().peephole(); + Node rmem = rctl==_code.XCTRL ? con(TypeMem.TOP) : _returnScope.mem(); + Node expr = rctl==_code.XCTRL ? con(Type.TOP) : _returnScope._inputs.last().peephole(); + ReturnNode ret = (ReturnNode)peep(new ReturnNode(rctl, rmem, expr, rpc, fun)); + fun.setRet(ret); + // Approximate function size, for inlining heuristics + fun._approxUIDs = _code.UID() - oldUID; + for( Node cend : ret.outs() ) + if( cend instanceof CallEndNode ) + throw TODO(); //_code.add(cend); // have call ends ALREADY? just barely made the Return + + // Export (and keep-alive) functions until prove dead + _ref.addFun(_code,fun); + + // Unstack parser state + if( _returnScope != null ) _returnScope.kill(); + _returnScope = returnScope; + _continueScope = continueScope; + _breakScope = breakScope; + _scope.pop(); + + return ret; + } + + private int updateSelfAsFieldsDiscovered( String typeName, int nvar ) { + int lex = _scope.klast()._lexSize + 2/*Skip self and selfMem*/; + // Load self type in and out of the TYPES hashtable. + TypeStruct tself = (TypeStruct)TYPES.get(typeName); + // Start looking for newly declared vars at the lexical scope start, + // skipping two ("self" and "selfmem") and fields seen already. + while( lex + nvar < _scope.nIns() ) { + Var v = _scope.var(lex + nvar++); + // Field with sharper type + Field old = tself.field(v._name); + Field fld = v._extern!=0 + ? Field.make(v._name,v.type(),v._extern,false,true) + : old == null + ? Field.make(v._name,v.type(),_code.alias(_ref._cname),v._final) + : old.makeFrom(v.type()); + tself = tself.addOrUpdate(fld); + } + TYPES.put(typeName,tself); + return nvar; + } + + + void upgradeSelfTypeAndStoreFields( String typeName, boolean isClz, ParmNode self, ParmNode smem, boolean upgrade ) { + int lex = _scope.klast()._lexSize; + assert _scope.var(lex)._name=="self"; + TypeStruct tself = (TypeStruct)TYPES.get(typeName); + int base = lex+2; // Skip ctrl, mem + if( upgrade ) { + // Default <init>/<clinit> defines the struct and can sharpen fields. + // A named constructor only initializes the already-closed struct. + for( int i=0; i<tself.nkids(); i++ ) { + Field tfld = tself._fields[i]; + int vidx = fieldVarIdx(base,tfld._fname); + if( vidx == -1 ) + continue; + Var v = _scope.var(vidx); + // Forward-ref fields where placed in tself but will get promoted + // to an outer scope - and should not be in tself. + if( v._fref ) + tself = tself.remove(i--); + else if( v.type() != tfld._t ) { + assert v.type().isa(tfld._t); + tself = tself.replace( tfld.makeFrom( v.type() ) ); + } + } + + // Close Struct type after parsing struct body. + TYPES.put( tself._name, tself = tself.close() ); + // Improve self, selfMem types in scope. + // Class self-type is a singleton pointer. Instance <init> receives the + // broad allocation pointer, while private memory tracks initialized fields. + self._declaredType = self._type = isClz ? TypeMemPtr.make((byte)2,tself,true) : TypeMemPtr.make(constructorRecv(tself)); + FunNode fun = self.fun(); + fun.setSig(fun.sig().makeFrom(self._type,0)); + if( !isClz ) // <init> returns upgraded private memory + smem._declaredType = smem._type = TypeMem.makePrivate(tself); + } + + if( !upgrade ) { + IdentityHashMap<Var,Type> exit = null; + if( _returnScope!=null && !_returnScope.ctrl()._type.isHigh() ) { + exit = new IdentityHashMap<>(); + int first = _scope._kinds.at(_scope.depth()-2)._lexSize+2; + for( int i=first; i<lex; i++ ) + exit.put(_scope.var(i),_returnScope.in(i)._type); + } + _ctorExits.add(exit); + } else if( !isClz ) { + checkInitFields(tself,base); + } + + // When can _returnScope be null here? A never-exit constructor will + // not have any returns, and thus no need to gather values and store + // them into the (never) constructed object + if( _returnScope==null ) + return; + + // A MemMerge to gather field updates as stores. Classes update the + // normal public memory. <init> updates the private memory that got + // passed in - is treated like a normal argument and not like memory. + Node mmm = isClz ? _returnScope.mem(1) : smem; + + + // Store constructor results into fields + for( int i=0; i<tself.nkids(); i++ ) { + Field fld = tself._fields[i]; + int vidx = upgrade + ? fieldVarIdx(base,fld._fname) + : fieldVarIdxBefore(lex,fld._fname); + if( vidx == -1 ) + continue; + Var v = _scope.var(vidx); + if( v._fref || v._extern!=0 ) continue; // C storage has no Simple initializer + // NewNode's TOP field is the zero-code poison for a required + // field. Leave it alone unless the declaration body supplied a + // value; otherwise a user constructor must initialize it. + Node val = _returnScope.in(vidx); + if( upgrade && v._uninit && + !initialized(val._type) ) continue; + // A numeric merge can still be untyped while parsing. Retain its + // declared conversion at the helper boundary, as assignments do. + if( fld._t instanceof TypeInteger || fld._t instanceof TypeFloat ) + val=convertExpr(val,fld._t); + // Store value into extended struct + Node prior = mmm.keep(); + Node st = peep(new StoreNode(null, fld._fname, fld._alias, fld._t, null, prior, self, off(tself,fld._fname), val, fld._final)); + mmm = mergeAlias(prior,fld._alias,st); + prior.unkeep(); + } + mmm = mmm.peephole(); + + if( !isClz ) + // Stuff private mem into normal expression return value. + _returnScope.setDef(_returnScope.nIns()-1,mmm); + else + // Parser memory is one opaque whole-memory edge. Publish the + // completed class-field aggregate as the <clinit> return memory. + _returnScope.mem(mmm); + //_code.add(mmm); + } + + // Validate each user constructor, or an implicit empty one, before the + // declaration scope goes away. Later fields use their declaration defaults. + private static boolean initialized(Type t) { + return t!=Type.BOTTOM && !t.isHigh() && (!(t instanceof TypeNil tn) || tn.notNull()); + } + + private void checkInitFields(TypeStruct ts, int base) { + if( _returnScope==null || _returnScope.ctrl()._type.isHigh() ) return; + boolean implicit = _ctorExits.isEmpty(); + if( implicit ) _ctorExits.add(new IdentityHashMap<>()); + for( IdentityHashMap<Var,Type> exit : _ctorExits ) { + if( exit==null ) continue; + for( Field fld : ts._fields ) { + int idx = fieldVarIdx(base,fld._fname); + if( idx == -1 ) continue; + Var v = _scope.var(idx); + if( !v._uninit || v._fref ) continue; + Type t = exit.getOrDefault(v,_returnScope.in(idx)._type); + if( !initialized(t) ) + throw error("'"+ts._name+"' is not fully initialized, field '"+fld._fname+ + (implicit ? "' needs to be set in a constructor" : + "' is only partially set in the constructor"),v._loc); + } + } + } + + private int fieldVarIdx( int base, String name ) { + for( int i=base; i<_scope.nIns(); i++ ) + if( _scope.var(i)._name == name ) + return i; + return -1; + } + + // Named constructors update field Vars from the enclosing initializer + // scope. Constructor arguments and locals begin at limit and must not + // shadow the field state gathered into private memory. + private int fieldVarIdxBefore( int limit, String name ) { + for( int i=limit-1; i>=0; i-- ) + if( _scope.var(i)._name == name ) + return i; + return -1; + } + + private boolean isConstructorDecl(String name) { + TypeStruct self = _scope.constructorSelf(); + return self != null && name == initFieldName(self._name); + } + + private boolean inExplicitConstructor() { + Kind.Func func = (Kind.Func)_scope._kinds.at(_scope.enclosingFuncOrDecl()); + return isExplicitConstructorName(func._name); + } + + private boolean isExplicitConstructorName(String funName) { + TypeStruct self = _scope.constructorSelf(); + return self != null && funName == (self._name+"."+initFieldName(self._name)).intern(); + } + + private static TypeStruct constructorRecv(TypeStruct self) { + Field[] fs = new Field[self._fields.length]; + for( int i=0; i<fs.length; i++ ) { + Field fld = self._fields[i]; + fs[i] = fld.makeFrom(fld._t.makeStorage()); + } + return TypeStruct.make(self._name,self._open,fs); + } + + + /** + * Parses a block + * + * <pre> + * '{' statements '}' + * </pre> + * Does not parse the opening or closing '{}' + * @return a {@link Node} or {@code null} + */ + private Node parseBlock(Kind kind) { + // Enter a new scope + _scope.push(kind); + Node last = _code.ZERO; + while (!peek('}') && !_lexer.isEOF()) { + _code.add(last); + 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( 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 (inStructDeclaration() && matchx("new")) return parseConstructorDeclaration(); + else if (matchx(";") ) return _code.ZERO; // Empty statement + // Break ambiguity around leading function types and starting a block + else if (peek('{') && !isTypeFun() ) { + match("{"); + return require(parseBlock(new Kind.Block()),"}"); + } + // Declaration or normal assignment/expression + else return parseDeclarationStatement(); + } + + private boolean inStructDeclaration() { + return _scope.klast() instanceof Kind.Func fun && FunNode.isInstance(fun._name); + } + + // Constructor declaration: new TypeName = { args -> body }; + private record Constructor(String name, Lexer body) {} + private Constructor _pendingCtor; + + private Node parseConstructorDeclaration() { + TypeStruct self = _scope.constructorSelf(); + assert self != null; + String name = requireId(); + String expect = initFieldName(self._name); + if( name != expect ) + throw error("Constructor name '" + name + "' must match type name '" + expect + "'"); + require("="); + if( _pendingCtor!=null ) throw error("Redefining constructor '"+name+"'"); + _pendingCtor=new Constructor(name,loc()); + // Parse the body after all field declarations are known, so self.x + // and x share a local slot even when x is declared below this body. + require("{"); + int depth=1; + while( depth>0 ) { + _lexer.skipWhiteSpace(); + if( _lexer.isEOF() ) throw error("Unclosed constructor body"); + char ch=_lexer.nextChar(); + if( ch=='{' ) depth++; + else if( ch=='}' ) depth--; + else if( ch=='"' || ch=='\'' ) { + char quote=ch; + while( !_lexer.isEOF() ) { + ch=_lexer.nextChar(); + if( ch=='\\' ) _lexer.nextChar(); + else if( ch==quote ) break; + } + } + } + require(";"); + return _code.ZERO; + } + + private void finishConstructor() { + if( _pendingCtor==null ) return; + Constructor ctor=_pendingCtor; + _pendingCtor=null; + Lexer after=_lexer; + _lexer=new Lexer(ctor.body()); + defineConstructor(ctor.name()); + _lexer=after; + } + + private void defineConstructor(String name) { + TypeStruct self=_scope.constructorSelf(); + Node ctor = constructorFunc(name); + TypeFunPtr sig = (TypeFunPtr)ctor._type; + String clzName = addClzPrefix(self._name); + TypeStruct clz = (TypeStruct)TYPES.get(clzName); + Field old = clz==null ? null : clz.field("<ctor>"); + if( old != null ) + throw error("Redefining constructor '" + name + "'"); + Field fld = Field.make("<ctor>",sig,_code.alias(_ref._cname),true); + TYPES.put(clzName,clz==null ? TypeStruct.make(clzName,true,fld) : clz.add(fld)); + require(ctor,";"); + } + + /** + * 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(new 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(),null,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 + CFGNode ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + CFGNode ifF = (CFGNode)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().isKill(); // 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.balanceLoopFRefs(_scope); + exit.balanceLoopFRefs(_scope); + 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. + // During sys parsing, there is no xscope here. + if( !_xScopes.isEmpty() ) { + _xScopes.pop(); + _xScopes.push( exit ); + } + _scope = exit; + return _code.ZERO; + } + + private ScopeNode jumpTo(ScopeNode toScope) { + ScopeNode cur = _scope.dup(); + ctrl(_code.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.depth() > _breakScope.depth() ) + 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.depth() <= _breakScope.depth(); + 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 _code.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((CFGNode)_breakScope.ctrl(), null, false); + return _code.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 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,"else"); + } + + // Parse a conditional expression, merging results. + private Node parseTrinary( Node pred, String fside ) { + // IfNode takes current control and predicate + pred.keep(); + Node ifNode = new IfNode(ctrl(), pred).peephole(); + // Setup projection nodes + CFGNode ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + CFGNode 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 + fScope.ctrl(ifF); // The pending arm already starts at False + _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 + // Parse true-side flavor + Node lhs = switch( fside ) { + case "else" -> parseStatement(); // if( pred ) stmts; + case ":" -> parseAsgn(); // pred ? asgn; + case "&&" -> parseLogical(); // pred && expr + case "||" -> pred; // pred || expr_is_ignored + default -> throw TODO(); + }; + lhs.keep(); + _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 + ifF.unkeep(); // fScope already owns the false control + // 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); + // Parse false-side flavor + boolean doRHS = false; // RHS is optional for if/else and trinary + Node rhs = switch( fside ) { + case "else" -> (doRHS=match(fside)) ? parseStatement() : con(lhs._type.makeZero()); + case ":" -> (doRHS=match(fside)) ? parseAsgn() : con(lhs._type.makeZero()); + case "&&" -> con(lhs._type.makeZero()); // pred && expr_is_ignored + case "||" -> parseLogical(); // pred || expr + default -> throw TODO(); + }; + rhs.keep(); + _scope.removeGuards(ifF); + // The RHS of `||` is parsed on the false side, so retain its updated + // control, memory and locals just as an explicit else arm does. + if( doRHS || fside.equals("||") ) + fScope = _scope; + pred.unkeep(); + + // Check the trinary widening int/flt + if( !fside.equals("else") ) { + 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("",r,lhs.unkeep(),rhs.unkeep())); + // Immediately fail e.g. `arg ? 7 : ptr` + ParseException err; + if( !fside.equals("else") && (err=ret.err()) !=null ) throw err; + ret.keep(); + r.peephole(); + return ret.unkeep(); + } + + /** + * 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(), ";"); + return addReturn(expr); + } + private Node addReturn(Node expr) { + expr.keep(); + // Need default memory, since it can be lazy, need to force + // a non-lazy Phi + mem(); // Force a pending lazy bulk-memory Phi + // For <init> and <clinit> - all fields in scope will be live-on-exit + // and stored into `self` and need to have lazy-phi's inserted. + int lexN = _scope.enclosingFuncOrDecl(); + Kind.Func k = (Kind.Func)_scope._kinds.at(lexN); + if( FunNode.isInit(k._name) ) { + int nestedLexSize = lexN+1 < _scope.depth() ? _scope._kinds.at(lexN+1)._lexSize : _scope.nIns(); + for( int i=k._lexSize; i<nestedLexSize; i++ ) + _scope.update(_scope.var(i),null); + if( _returnScope != null ) { + Var vexpr = _returnScope._vars.pop(); + Node oldX = _returnScope.removeLast(); + int rlen = _returnScope.nIns(); + while( rlen < nestedLexSize ) { + _returnScope._vars.add(_scope.var(rlen)); + _returnScope.addDef(con(_scope.var(rlen++).type().makeZero())); + } + _returnScope._vars.add(vexpr); + _returnScope.addDef(oldX); + } + } else if( isExplicitConstructorName(k._name) ) { + // Field bindings live in the enclosing declaration, not among the + // constructor's arguments/locals. Force lazy Phis on every exit. + int first = _scope._kinds.at(lexN-1)._lexSize+2; + for( int i=first; i<k._lexSize; i++ ) + _scope.update(_scope.var(i),null); + } + + // No prior merge point? Just clone and hang on to it + if( _returnScope == null ) { + _returnScope = _scope.dup(); + while( lexN+1 < _returnScope.depth() ) + _returnScope._pop(); // Pop a nested block scope until we hit the function scope + _returnScope.define("$expr", inferredReturnType(expr._type), true, expr, null); + + } else { + // For <init> and <clinit> - ALL FIELDS IN LAST SCOPE are live + // and need to have lazy-phi's inserted. + + // And fields might not match.... will need matching Var/define with default values + int rlen = _returnScope.nIns()-1; + RegionNode r = ctrl(new RegionNode(null, null,_returnScope.ctrl(), _scope.ctrl()).init().keep()); + _returnScope._merge(_scope, r, rlen); + Node oldExpr = _returnScope.in(rlen); + _returnScope.setDef(rlen,new PhiNode("$expr", r, oldExpr, expr).peephole()); + _code.add(r); + _returnScope.ctrl(r.unkeep()); + } + ctrl(_code.XCTRL); // Kill control + return expr.unkeep(); + } + + /** 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(); } + + // Find variable to update + Var def = _scope.lookup(name); + if( def==null ) + def = defineOpenConstructorField(name); + if( def==null ) + throw error("Undefined name '" + name + "'"); + + if( def._extern!=0 ) // Parse the C binding as a memory lvalue + { pos(old); return parseExpression(); } + + // 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 an allocation, final assign is OK, outside nope. + // The alloc() call added the allocation scope + !_scope.canInit(def) ) + throw error("Cannot reassign final '"+name+"'"); + checkSharedFunction(def); + + // Parse assignment expression + Node expr = parseAsgn(); + + // Lift expression, based on type + Type decl = def.type(); // Declared type + Node lift = convertExpr(expr.keep(), decl).keep(); + // Update + _scope.update(name,lift); + + // Check for storing into an instance field instead of a local var + int kx = _scope.kindx(def); // Declaration scope + int fx = _scope.enclosingFunction(); // Enclosing function scope + Kind kk = _scope._kinds.at(kx); + Kind fk = _scope._kinds.at(fx); + // Access instance field from 'self' + if( kx+1 == fx && kk instanceof Kind.Func inst && FunNode.isInstance(inst._name) && + fk instanceof Kind.Func method && !inExplicitConstructor() ) { + // Named field offset on an unknown struct type + TypeMemPtr self = (TypeMemPtr)_scope.var(fk._lexSize).type(); + TypeStruct sobj = (TypeStruct)TYPES.get(self._obj._name); + Field fld = sobj.field(name); + int alias = fld._alias; + Node ptr = _scope.in(method._lexSize); + Node off = fldoff(ptr,name); + Node prior = mem().keep(); + Node st = new StoreNode(loc(), name, alias, decl, ctrl(), prior, ptr, off, lift, inExplicitConstructor()).peephole().keep(); + storeMem(st,alias,prior); + st.unkeep(); + prior.unkeep(); + } + lift.unkill(); + + + // Return un-lifted expr + return expr.unkeep(); + } + + // Widen ints to floats; narrow wide int types. + // Early error if types do not match variable. + private Node convertExpr( Node expr, Type t ) { + if( !expr._type.accessISA(t) ) + throw error("Type "+expr._type.str()+" is not of declared type "+t.str()); + if( t instanceof TypeMemPtr tmp && tmp.isFinal() ) + expr = peep(new ReadOnlyNode(expr)); + // A FunPtr carries the semantic edge to its function's Return. Do not + // hide that edge behind a Convert which can fold to a plain constant. + if( expr instanceof FunPtrNode && t instanceof TypeFunPtr ) + return expr; + // Auto-widen array to i64 (cast ptr to raw int bits) + if( t == TypeInteger.BOT && expr._type instanceof TypeMemPtr tmp && tmp._obj.isAry() ) + return peep(new AddNode(peep(new PtrToIntNode(expr)),off(tmp._obj,"[]"))); + // Widen ints to floats, narrow wide types + return peep(new ConvertNode(t,expr)); + } + + // Inferred `var` declarations describe writable slots, not the exact + // value used to initialize them. Explicitly typed locals retain the + // user's nullability and numeric-width guarantees; `val` retains the + // initializer's precise type. + private static Type mutableInferredType(Type t) { + return switch( t ) { + case TypeInteger ti -> TypeInteger.BOT; + case TypeFloat tf -> TypeFloat.F64; + case TypeMemPtr tmp -> tmp.makeVar(); + case TypeFunPtr tfp -> tfp; + case TypeConAry<?> a -> a; + case TypePtr ptr -> TypePtr.PTR; + case TypeScalar ts -> TypeScalar.BOT; + default -> t; + }; + } + + private Type constructorFieldType(Type t) { + if( t instanceof TypeMemPtr ptr && ptr._obj.isConAry() ) { + TypeStruct declared=(TypeStruct)TYPES.get(ptr._obj._name); + TypeStruct shape=typeAry(declared._fields[1]._t,ptr._obj._fields[1]._final); + return TypeMemPtr.make((byte)2,shape,false).withAccess(ptr._ro); + } + return mutableInferredType(t); + } + + // Seed an inferred return merge with the declared numeric family instead + // of a transient constant/range. Pointer nullability remains part of the + // inferred function result and is combined by the later Phi. + private static Type inferredReturnType(Type t) { + return switch( t ) { + case TypeInteger ti -> TypeInteger.BOT; + case TypeFloat tf -> TypeFloat.F64; + default -> t; + }; + } + + 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() { + int old = pos(); + Type t = type(); + if( peek('.') ) // Ambiguity static vars: "type.var", parse as expression + { pos(old); t=null; } + 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,";"); + } + + // The binding modifier sits immediately before the name. It never + // changes permissions on the referenced object. + private boolean bindingFinal(Type t) { + if( match("!") ) return false; + if( match("~") ) return true; + return t==Type.TOP || t instanceof TypeNil; + } + + /** Parse final: [!|~]var['=' asgn] + */ + private Node parseDeclaration(Type t) { + assert t!=null; + // Has var/val instead of a user-declared type + boolean inferType = t==Type.TOP || t==Type.BOTTOM; + boolean xfinal = bindingFinal(t); + Lexer loc = loc(); + String name = requireId(); + // Optional initializing expression follows + boolean fld_final = false; // Field is final, but not deeply final + boolean uninit = false; // Required non-null constructor field + Node expr; + if( match("=") ) { + if( isExternDecl() ) { + if( !(t instanceof TypeFunPtr) ) { + Node ptr = externData(name,t); + // The lexical binding is a fixed address; its contents are mutable. + if( !_scope.define(name,t,true,ptr,loc) ) + throw error("Redefining name '"+name+"'",loc); + _scope.lookup(name)._extern = _code.externAlias(name); + return _code.ZERO; + } + expr = externDecl(name,t); + t = expr._type; // Upgrade declared type to the exact extern decl type + } else if( isConstructorDecl(name) ) { + expr = constructorFunc(name); + } else { + expr = parseAsgn(); + } + // 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 = mutableInferredType(t); + // Instance fields may change until the constructor finishes. + // Keep the declared family/access, not the default's constant + // value or string contents. Class constants remain static. + else if( _scope.klast() instanceof Kind.Func fun && FunNode.isInstance(fun._name) && !(t instanceof TypeFunPtr) ) + t = constructorFieldType(t); + } + + // expr is a constant function + if( t instanceof TypeFunPtr decl && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) { + t = decl.makeFrom(tfp.fidx()); + FunNode fun = _code.link(tfp); + if( fun != null ) + fun.setName(name); // Assign debug name to Simple function + } + + } else { + // Since no initializer, not-final but might need an initializer + + // 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.inConstructor() ) + throw errorSyntax("=expression"); + // Initial value for uninitialized struct fields. + expr = switch( t ) { + // Required pointers retain their declared shape, but include + // null until every constructor path assigns them. + case TypeNil tn -> tn.nullable() ? con(Type.NIL) : con(t.meet(Type.NIL)); + case TypeInteger ti -> xfinal ? con(Type.BOTTOM) : _code.ZERO; + case TypeFloat tf -> con(xfinal ? Type.BOTTOM : 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); } + }; + // Every non-null pointer without a default is constructor-required. + // Without `!` it is also shallow-final; mutability and definite + // initialization are independent properties. + if( (xfinal && !(t instanceof TypeNil)) || (t instanceof TypeNil tn && !tn.nullable()) ) { + uninit = true; + if( xfinal ) fld_final = true; + } + } + + // Lift expression, based on type + // Do not let an authoritative Convert hide the nullable + // not-yet-initialized state from constructor-local type flow. + Node lift = uninit ? expr : convertExpr(expr, t); + + // Define a new name + if( !_scope.define(name,t,xfinal || fld_final,lift, loc) ) + throw error("Redefining name '" + name + "'", loc); + _scope.lookup(name)._uninit = uninit; + return lift; + } + + + /** + * Parse a struct declaration, and return the following statement. + * Structs cannot be redefined, but can be nested. + * + * @return zero + */ + private Node parseStruct( ) { + // keyword "struct" already parsed, so expect the struct name next. + String typeName = requireId(); + require("{"); + + // create type class:NESTED.typeName; no fields. + + String old = _nestedType; + String fullName = _scope.depth()==1 && _nestedType.endsWith(typeName) ? old : (old+"."+typeName).intern(); + _nestedType = fullName; // Recursive structs start with this as their basename + + ReturnNode ret = parseStruct( false, fullName ); + + _nestedType = old; + + // Insert a field into class:NESTED.typeName of "val typeName = fcn-ptr-to-init" + // Function does malloc internally. + // Future self: means subtyping has to deal with "who does the malloc" + String clzName = addClzPrefix(fullName); + TypeFunPtr sig = ret.fun().sig(); + TypeStruct ts = (TypeStruct)TYPES.get(clzName); + String initName = initFieldName(fullName); + Field oldFld = ts==null ? null : ts.field(initName); + Field fld = oldFld==null + ? Field.make(initName,sig,_code.alias(_ref._cname),true) + : oldFld.makeFrom(sig); + Field ctorFld = ts==null ? null : ts.field("<ctor>"); + // A same-named struct shares its file's class object and flag. + Field flag = ts==null ? null : ts.field("$init"); + ts = ctorFld==null + ? TypeStruct.make(clzName,false,fld) + : TypeStruct.make(clzName,false,fld,ctorFld); + if( flag!=null ) ts=ts.add(flag); + TypeFunPtr alloc = makeAllocator((TypeStruct)TYPES.get(fullName),ret,ctorFld); + ts = ts.add(Field.make("<new>",alloc,_code.alias(_ref._cname),true)); + TYPES.put(clzName,ts); + + // Insert a field in the containing class with the nested class type. + // This is basically sugar for "val CLZNAME = <init>" + // and is a nicer version of updateSelfAsFieldsDiscovered. + _scope.define(typeName, sig, true, new FunPtrNode(sig,_code._start,ret).peephole(), loc()); + + require("}"); + return require(_code.ZERO,";"); + } + + // Build the declaration-side allocation helper. Allocation sites only + // call this function; all layout, initialization and publication choices + // are made here, where the complete struct and constructor are known. + private TypeFunPtr makeAllocator(TypeStruct ts, ReturnNode initRet, Field ctorFld) { + TypeFunPtr ctor = ctorFld==null ? null : (TypeFunPtr)ctorFld._t; + int nargs = ctor==null ? 0 : ctor.nargs()-2; // Hide self and selfMem + Type[] atypes = new Type[nargs]; + for( int i=0; i<nargs; i++ ) atypes[i] = ctor.arg(i+2); + TypeMemPtr ptr = TypeMemPtr.make(ts); + TypeFunPtr sig = TypeFunPtr.make1((byte)2,true,atypes,ptr,_code.fidx(_ref._cname)); + String name = (ts._name+".<new>").intern(); + + int oldUID = _code.UID(); + FunNode fun = (FunNode)new FunNode(loc(),sig,name,_ref,null,_ref._start).peephole(); + _code.link(fun); + Node rpc = new ParmNode("$rpc",0,TypeRPC.BOT,fun,ConstantNode.seed(TypeRPC.BOT).peephole()).peephole(); + Node pub = new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun, + new ProjNode(_ref._start,1,ScopeNode.MEM0).peephole()).peephole(); + Node[] parms = new Node[nargs]; + for( int i=0; i<nargs; i++ ) + parms[i] = new ParmNode("arg"+i,i+2,atypes[i],fun,ConstantNode.seed(atypes[i]).peephole()).peephole(); + + NewNode nnn = new NewNode(ts,fun,off(ts," len")).init(); + Node self = new ProjNode(nnn,0,ts.str()).init(); + Node priv = new ProjNode(nnn,1,"#selfMem").init(); + Node[] initArgs = new Node[]{fun,pub,self,priv, + new FunPtrNode(initRet.fun().sig(),_code._start,initRet).peephole()}; + CallEndNode cend = rawCall(initArgs,initRet.fun()); + Node ctl = new CProjNode(cend,0,ScopeNode.CTRL).peephole(); + pub = new ProjNode(cend,1,ScopeNode.MEM0).peephole(); + priv = new ProjNode(cend,2,"#selfMem").peephole(); + + if( ctor != null ) { + Node[] cargs = new Node[nargs+5]; + cargs[0]=ctl; cargs[1]=pub; cargs[2]=self; cargs[3]=priv; + for( int i=0; i<nargs; i++ ) cargs[i+4]=parms[i]; + FunNode cfun = _code.link(ctor); + cargs[cargs.length-1] = new FunPtrNode(ctor,_code._start,cfun.ret()).peephole(); + cend = rawCall(cargs,cfun); + ctl = new CProjNode(cend,0,ScopeNode.CTRL).peephole(); + pub = new ProjNode(cend,1,ScopeNode.MEM0).peephole(); + priv = new ProjNode(cend,2,"#selfMem").peephole(); + } + + priv.keep(); + for( Field field : ts._fields ) { + Type storage = field._t.makeStorage(); + Field escaped = field._final ? field : field.makeFrom(storage).makeFrom(true); + pub.keep(); + Node esc = new EscapeNode(escaped,self,priv,pub).peephole(); + MemMergeNode merge = new MemMergeNode(null,pub); + merge.alias(field._alias,esc); + Node next = merge.peephole(); + pub.unkeep(); + pub = next; + } + priv.unkill(); + ReturnNode ret = (ReturnNode)new ReturnNode(ctl,pub,self,rpc,fun).peephole(); + fun.setRet(ret); + fun._approxUIDs = _code.UID() - oldUID; + _ref.addFun(_code,fun); + return sig; + } + + private CallEndNode rawCall(Node[] args, FunNode target) { + // Establish this known declaration-side edge immediately, but do not + // peephole or inline it while the surrounding factory is being built. + // IterPeeps therefore starts with the complete initializer caller set. + CallNode call = new CallNode(loc(),args).init(); + CallEndNode cend = new CallEndNode(call,TypeRPC.constant(_code.rpc(_ref._cname))).init(); + call.link(target); + return cend; + } + + // Parse a struct declaration (not an allocation); file-level is a class-init, and scope structs are normal + private ReturnNode parseStruct( boolean isClz, String typeName ) { + var oldExits = _ctorExits; + _ctorExits = new ArrayList<>(); + // Record & restore global state set during parsing the <init> code + Node oldCtrl= _scope.ctrl().keep(); + Node oldMem = _scope.mem ().keep(); + + // Make the future clazz/instance struct. + TypeStruct tself = (TypeStruct)TYPES.get(typeName); + if( tself==null ) + TYPES.put(typeName, tself = TypeStruct.make(typeName,true)); + else { + assert tself._open; + TYPES.put(typeName,tself = tself.defined()); + } + + // Class object keeps a runtime flag to signal the <clinit> has run. + if( isClz ) + TYPES.put(typeName,tself=tself.add(Field.make("$init",TypeInteger.BOOL,_code.alias(_ref._cname),false))); + + // Signature of the default constructor + TypeFunPtr sig = structInitSig(isClz,tself); + // Function and argument names of default constructor + String fname = (typeName + (isClz ? ".<clinit>" : ".<init>")).intern(); + String[] ids = new String[]{"self", isClz ? "arg" : "#selfMem"}; + + // Struct decls look like function bodies. Parse function body normally. + ReturnNode ret = _parseFunctionBody(fname,sig,loc(),ids); + _ctorExits = oldExits; + // Unwind global state. + ctrl(oldCtrl.unkeep()); + mem (oldMem .unkeep()); + return ret; + } + + // <cl/init> signature: { self arg/selfMem -> BOT/selfMem } + // Self-memory is the rare *private* (never aliased) memory for the new object. + private TypeFunPtr structInitSig( boolean isClz, TypeStruct tself ) { + // Private uninitialized memory from new object, no escaped values. + TypeMem privMem = isClz ? null : TypeMem.make(1,tself,true,isClz,false,null,null); + Type targ = isClz ? TypeInteger.BOT : privMem; + // Return for private memory has to name all escaped fields, which are not known yet. + // Escape ALL fields, and sharpen later. + Type tret = isClz ? Type.BOTTOM : TypeMem.makePrivate(tself); + Type[] targs = new Type[]{ TypeMemPtr.make((byte)2,tself,isClz), targ }; + // FIDX for FREFs is in the global namespace, needs the correct clz name. + String clzBare = tself._name; + int order = 3; // 3 is always the <init> order, 2nd in the file just behind the <clinit> + if( isClz ) { + assert startsClzPrefix( clzBare ); + clzBare = clzBare.substring(clzPrefix.length()).intern(); + order = 2; // 2 is always the <clinit> order, first in file and skipping reserved #1 + } + return TypeFunPtr.make1((byte)2, true, targs, tret, _code.fidx(clzBare,order)); + } + + // A class type carries the constructor function as a required final field. + private TypeStruct classStruct( String typeName, TypeFunPtr init, boolean open ) { + return TypeStruct.make(addClzPrefix(typeName),open,Field.make(initFieldName(typeName),init,_code.alias(_ref._cname),true)); + } + + private static String initFieldName( String typeName ) { + if( startsClzPrefix(typeName) ) + typeName = typeName.substring(clzPrefix.length()); + int idx = typeName.lastIndexOf('.'); + return (idx == -1 ? typeName : typeName.substring(idx+1)).intern(); + } + + private TypeStruct latestStruct( TypeStruct ts ) { + Type t = TYPES.get(ts._name); + return t instanceof TypeStruct ts2 ? ts2 : ts; + } + + private Field fieldOrOpen( TypeStruct ts, String name, boolean xfinal ) { + ts = latestStruct(ts); + Field fld = ts.field(name); + if( fld != null ) return fld; + if( !ts._open || !ts._fref ) return null; + fld = Field.make(name,Type.BOTTOM,_code.alias(_ref._cname),xfinal); + TYPES.put(ts._name,ts = ts.add(fld)); + if( _ctorOpenStruct != null && _ctorOpenStruct._name == ts._name ) + _ctorOpenStruct = ts; + return fld; + } + + private Var defineOpenConstructorField( String name ) { + if( _ctorOpenStruct==null ) return null; + Field fld = fieldOrOpen(_ctorOpenStruct,name,true); + if( fld == null ) return null; + _scope.define(name,fld._t,fld._final,con(Type.TOP),loc()); + return _scope.lookup(name); + } + + + // 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[?] + + // Structs have nested names, and can be discovered with a partial name + // match. Example, searching for a type name "C.D" in the namespace "A.B". + // This will match "A.B.C.D", then "A.C.D", then "C.D". + + // Will also match external names like "sys.io" or "externModule.subtype.subtype" + + private Type type() { + // Only type with a leading `{` is a function pointer... + if( peek('{') ) return typeFunPtr(); + + // Otherwise you get a type name + int old1 = pos(); + boolean writable = match("!"); + boolean readonly = !writable && match("~"); + String tname = _lexer.matchId(); + if( tname==null ) return posT(old1); + + // Convert the type name to a type. + Type t0 = TYPES.get(tname), t1 = t0; + // No new types as keywords + if( KEYWORDS.contains(tname) ) { + if( t0 == null ) return posT(old1); + // Something like "int" or "f64" or "var". + // Can be "int[]" so still need to check array-ness + } else { + if( t0 == Type.BOTTOM || t0 == Type.TOP ) return t0; // var/val type inference + + if( t0 != null ) { + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + sname = tname+"."+sname; + Type t = TYPES.get(sname); + if( t==null ) { pos(old2); break; } + tname = sname; + t0 = t; + } + } else { + // Type A.B.C starts with just A. Search our nested type path, + // e.g. path "X.Y.Z", in order: + + // If A==X its now a local type. + // If another dot, repeat for B==Y then C==Z + // Else if AX.B exists, done + // Else AX.B is a local forward ref. + // + // Else its a short cut form, one of: + // X.Y.Z.A + // X.Y.A + // X.A + // Else this all failed, assume a forward ref with A as an external symbol: + // A + + String root = _nestedType; + int ridx = root.indexOf('.'); + if( ridx != -1 ) root = root.substring(0,ridx); + if( tname.equals(root) ) { + boolean localForward = false; + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + sname = tname+"."+sname; + Type t = TYPES.get(sname); + if( t==null && !(_nestedType.equals(tname) || _nestedType.startsWith(tname+".")) && !localForward ) { + pos(old2); + break; + } + tname = sname; + t0 = t; + localForward |= t==null; + } + } else { + String nest = _nestedType, fullq = tname; + while( true ) { + fullq = nest+"."+tname; // Full qualified name + t0 = TYPES.get(fullq); + if( t0 != null ) + { tname = fullq; break; } // Search succeeded + // Subtract a layer from nested typenames + int idx = nest.lastIndexOf('.'); + if( idx== -1 ) break; // Search failed; leave as an external symbol + nest = nest.substring(0,idx); + } + if( t0==null ) { + CompUnit cu = ParseAll.findCompUnit(_code,_ref,tname); + if( cu != null ) { + tname = cu._cname; + t0 = TYPES.get(tname); + } + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + tname = tname+"."+sname; + t0 = TYPES.get(tname); + } + } else { + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + sname = tname+"."+sname; + Type t = TYPES.get(sname); + if( t==null ) { pos(old2); break; } + tname = sname; + t0 = t; + } + } + } + } + + tname = tname.intern(); + // Still no type found? Assume forward reference + t1 = t0 == null + ? TypeStruct.forward(tname) // Null: assume a forward ref type + : t0; + } + + // Structs are always actually references + if( t1 instanceof TypeStruct ts1 ) + t1 = TypeMemPtr.make(ts1); + + // A qualifier before the name applies to the struct reference, not + // to an enclosing array. Arrays have their own [] / [~] qualifier. + if( t1 instanceof TypeMemPtr tmp ) { + if( readonly || !writable ) t1 = tmp.makeRO(); + } else if( writable || readonly ) + return posT(old1); + + // Nest arrays and '?' as needed + Type t2 = t1; + while( true ) { + if( match("?") ) { + if( t2 instanceof TypeMemPtr tmp ) { + if( tmp.nullable() ) throw error("Type "+t2+" already allows null"); + t2 = tmp.makeNullable(); + } else + throw error("Type "+t0+" cannot be null"); + } else if( match("[~]") ) { + t2 = TypeMemPtr.make(typeAry(t2,true)).makeRO(); + } else if( match("[]") ) { + t2 = TypeMemPtr.make(typeAry(t2,false)); + } 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(); + if( !match("!") ) match("~"); + String id = _lexer.matchId(); + if( !(peek(',') || peek(';') || peek('=') || match("->")) || id==null ) + return posT(old1); // Reset lexer to reparse + pos(old2); // Reset lexer to reparse + // Yes a forward ref, so declare it + forwardRefType(tname,(TypeStruct)((TypeMemPtr)t1)._obj); + // Return the (array, final) type + return t2; + } + + private TypeMemPtr forwardRefType( String tname, TypeStruct tself ) { + TYPES.put(tname,tself); + String clzName = addClzPrefix( tname ); + TYPES.put(clzName, classStruct(tname,structInitSig(false,tself),true)); + return TypeMemPtr.make(tself); + } + + private TypeMemPtr forwardRefType( String tname ) { + tname = fullTypeName(tname); + return forwardRefType(tname,TypeStruct.forward(tname)); + } + + private String fullTypeName( String tname ) { + String nest = _nestedType, fullq; + while( true ) { + fullq = nest+"."+tname; + if( TYPES.get(fullq) != null ) + return fullq.intern(); + int idx = nest.lastIndexOf('.'); + if( idx== -1 ) return fullq.intern(); + nest = nest.substring(0,idx); + } + } + + // Make an array type of t. Always record a mutable version, + // but return the requested version. + private TypeStruct typeAry( Type t, boolean efinal ) { + // Non-null reference arrays are valid types, even though an ordinary + // zero-filled `new T[len]` cannot construct one. Allocation retains + // that check in alloc(); trusted runtimes (notably argv construction) + // may fill the private array completely before publishing it. + String tname = ("[]"+t.str()).intern(); + TypeStruct ta = (TypeStruct)TYPES.get(tname); + if( ta==null ) { + // Primitive arrays use the null compunit + String rcname = TYPES.get(t) instanceof TypeStruct ? _ref._cname : ("_."+t).intern(); + ta = TypeStruct.makeAry(tname,TypeInteger.U32,_code.alias(rcname),t,_code.alias(rcname), false ); + TYPES.put(tname,ta); + } + // Already have the aliases, just efinal is wrong + return TypeStruct.makeAry(tname,TypeInteger.U32,ta._fields[0]._alias,t,ta._fields[1]._alias,efinal ); + } + + private Type implicitSelfType() { + TypeStruct self = _scope.constructorSelf(); + // Ordinary instance methods accept the declared object shape. The + // constructor-only receiver starts at the low/default field values, + // and is built explicitly by constructorFunc(). + return self == null ? TypePtr.PTR : TypeMemPtr.make(self); + } + + // 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 + Ary<Type> ts = new Ary<>(Type.class); + // Always reserve a receiver slot. Outside constructor scopes it is + // broad and unused, so call sites can uniformly pass their self. + ts.push(implicitSelfType()); + if( match("}") ) // No-arg function { -> type } + return TypeFunPtr.make(match("?"),ts.asAry(),t0); + 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("?"),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 = parseLogical(); + return match("?") ? parseTrinary(expr,":") : expr; + } + + /** + * Parse an bitwise expression + * + * <pre> + * bitwise : compareExpr (('&' | '|' | '^') compareExpr)* + * </pre> + * @return a bitwise expression {@link Node}, never {@code null} + */ + + private Node parseLogical() { + Node lhs = parseBitwise(); + while (true) { + if( false ) ; + else if( match("&&") ) lhs = parseTrinary(lhs, "&&"); + else if( match("||") ) lhs = parseTrinary(lhs, "||"); + else break; + } + return lhs; + } + + private Node parseBitwise() { + Node lhs = parseEquality(); + while( true ) { + if( false ) ; + else if( matchOp('&') ) lhs = new AndNode(loc(),lhs,null); + else if( matchOp('|') ) lhs = new OrNode(loc(),lhs,null); + else if( match ("^") ) lhs = new XorNode(loc(),lhs,null); + else break; + lhs.setDef(2,parseEquality()); + lhs = peep(lhs); + } + return lhs; + } + + + /** + * Parse an eq/ne expression + */ + private Node parseEquality() { + Node lhs = parseComparison(); + while( true ) { + boolean eq = false; + if( match("==") ) eq = true; + else if( !match("!=") ) break; + lhs.keep(); + Node rhs = parseComparison(); + lhs = peep(new BoolNode.EQ(lhs.unkeep(),rhs)); + if( !eq ) lhs = peep(new NotNode(lhs)); + } + return lhs; + } + + /** + * Parse an expression of the form: + * + * <pre> + * expr : shiftExpr < shiftExpr <= shiftExpr... + * expr : shiftExpr > shiftExpr >= shiftExpr... + * </pre> + * @return an comparator expression {@link Node}, never {@code null} + */ + private Node parseComparison() { + Node lhs = parseShift(); + int dir = parseCompDir(); + // No compare + if( dir==0 ) return lhs; + lhs.keep(); + + // Compare + Node rhs = parseShift().keep(); + Node cmp = makeCompBool(dir,lhs.unkeep(),rhs).keep(); // Convert to a bool + + // Stacked compares? + int dir0 = parseCompDir(); + if( dir0 == 0 ) { + rhs.unkill(); + return cmp.unkeep(); + } + + // rhs is keeped() and becomes lhs + // cmp is NOT keeped() and is the last test + Node ifNode = new IfNode(ctrl(), cmp.unkeep()).peephole(); + Node ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + Node ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole(); + // False side does nothing but capture memory & side-effects + ScopeNode fail = _scope.dup(); + fail.ctrl(ifF); + + // Loop over stacked compares. Each loop moves the old RHS to become + // the new LHS, so e.g. (a < b < c < d) compares (LHS:a < RHS:b) then + // compares (LHS:b < RHS:c) then (LHS:c < RHS:d), etc. + while( dir0 != 0 ) { + if( Math.abs(dir) != Math.abs(dir0) ) + throw error("Mixing relational directions in a chained relational test"); + // True side parses next arm of test + ctrl(ifT.unkeep()); + lhs = rhs; // lhs is keeped() and is old RHS + rhs = parseShift().keep(); + cmp = makeCompBool(dir,lhs.unkeep(),rhs); // Convert to a bool + ifNode = new IfNode(ctrl(), cmp).peephole(); + ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole(); + // Merge result into the fail case + ctrl(ifF); + fail.mergeScopes(_scope.dup(), loc()).peephole(); + // Check next stacked compare + dir0 = parseCompDir(); + } + rhs.unkill(); + ctrl(ifT.unkeep()); + RegionNode r = fail.mergeScopes(_scope, loc()).init(); + _scope = fail; + return new PhiNode("", r,_code.ZERO,con(1)).peephole(); + } + + private int parseCompDir() { + if( match("<=") ) return -1; // Negative for "equals-or-less" + if( match(">=") ) return -2; // 1 for <, 2 for > + if( match("<" ) ) return 1; // Positive for just "less" + if( match(">" ) ) return 2; + return 0; + } + + private Node makeCompBool( int dir0, Node lhs, Node rhs ) { + // Convert to a bool + if( Math.abs(dir0)==2 ) // Swap direction + { Node tmp = lhs; lhs = rhs; rhs = tmp; } + lhs = peep(dir0 < 0 ? new BoolNode.LE(lhs,rhs) : new BoolNode.LT(lhs,rhs)); + return peep(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); + } + 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); + } + 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); + } + 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; // Inc vs Dec + String name = _lexer.matchId(); + if( name!=null ) { + Var n = _scope.lookup(name); + if( n != null && !(n.type() instanceof TypeMemPtr) ) { + if( n._extern!=0 ) + return parsePostfixName(_scope.in(n),name,delta); + if( n._final && !_scope.canInit(n) ) + throw error("Cannot reassign final '"+n._name+"'"); + checkSharedFunction(n); + Node expr = convertExpr(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())); + if (match("!")) return peep(new NotNode(parseUnary())); + // Else be a primary expression + return parsePrimary(); + } + + /** + * Parse a primary expression: + * + * <pre> + * primaryExpr : integerLiteral | "string" | 'char' | true | false | null | + * new Type | '(' expression ')' | Id['++','--'] | + * type['.' Id]* + * </pre> + * @return a primary {@link Node}, never {@code null} + */ + private Node parsePrimary() { + if( _lexer.isNumber(_lexer.peek()) ) return parseLiteral(); + if( _lexer.peek('"') ) return parsePostfix(newString(parseString())); + if( matchx("true" ) ) return con(1); + if( matchx("false") ) return _code.ZERO; + if( matchx("null" ) ) return con(Type.NIL); + if( match ("'" ) ) return parseChar(); + if( match ("(" ) ) return parsePostfix(require(parseAsgn(), ")")); + if( matchx("new" ) ) return parsePostfix(alloc()); + if( match ("{" ) ) return parsePostfix(require(func(),"}")); + + int pos = pos(); + // Expect an identifier now + String id = _lexer.matchId(); + if( id == null || KEYWORDS.contains(id) ) + throw errorSyntax("an identifier or expression"); + int pos2 = pos(); + // Attempt a local var lookup first + Var var = _scope.lookup(id); + if( var==null ) { + // Not a local var, try again as a type name + pos(pos); + Type t = type(); + if( t!=null ) { + // Static <clinit> typename lookup + if( peek('.') ) // type.fld, which is really a field lookup in the class object + return parsePostfix(con(t)); + // Direct reference to a <clinit> type. Convert the instance + // type to a class type name + if( !(t instanceof TypeMemPtr tmp) ) { + //return new FRefNode(addClzPrefix(tmp._obj._name), loc()).peephole(); + // Not a local var, but yes a bare type... disallow a local + // FREF and return null, not-a-primary parse. + pos(pos); + return null; + } + } + + // Insert FREF outside enclosing Kind.Func scope. + pos(pos2); + t = FRefNode.FREF_TYPE; + // Check if this is a class. + CompUnit cu = ParseAll.findCompUnit(_code,_ref,id); + if( cu!=null ) { + // Must be a final class-type + String clzName = addClzPrefix(cu._cname); + TypeStruct clz = (TypeStruct)TYPES.get(clzName); + if( clz == null ) + TYPES.put(clzName,clz = TypeStruct.make(clzName,true)); + t = TypeMemPtr.make((byte)2,clz,true); + } + // Define the FRef + var = _scope.defineFRef(id,t,true,loc()); + _frefs.push((FRefNode)_scope.in(var)); + } + + // Load local value + Node rvalue = _scope.in(var); + if( var._extern!=0 ) + return parsePostfixName(rvalue,id,0); + if( var._fref ) + return parsePostfix(rvalue); + // A constructor may read the unfinished object, but each field it + // reads must already be initialized on every path. Read the current + // local value; other fields may still be waiting for their first write. + if( inExplicitConstructor() && var._uninit && missingInit(rvalue,new BitSet()) ) + throw error("Field '"+id+"' might not be initialized",var._loc); + + // Check for an instance field load + int kx = _scope.kindx(var); // Declaration scope + int fx = _scope.enclosingFunction(); // Enclosing function scope + Kind kk = _scope._kinds.at(kx); + Kind fk = _scope._kinds.at(fx); + // Access instance field from 'self' + if( kx+1 == fx && kk instanceof Kind.Func inst && FunNode.isInstance(inst._name) && fk instanceof Kind.Func method && !inExplicitConstructor() ) + return parsePostfixName(_scope.in(method._lexSize),id,0); + + // Check for a function-escaping variable; these require true + // closures. Final constants are OK; final vars require a hidden var + // argument; not-final fields can just use an explicit struct arg. + if( kx < fx && !inExplicitConstructor() && // Out of scope + !(var._final && rvalue._type.isConstant()) ) { // Not a final constant + if( kx > 0 ) // Global scope is OK, only ever one of these (not one per function invoke) + throw error("Variable '"+var._name+"' is out of function scope and must be a final constant"); + + // var.idx==2 is self argument, 3 is args, 4 or more are now static globals + if( var._idx >= 4 ) { + // Read from global scope. + // Surely a nicer way to get the top-level scope compile name? + Var clzptr = _scope.var( 2 ); + // Get the most recent sharpen global + TypeStruct clz = (TypeStruct) TYPES.get( ((TypeMemPtr) clzptr.type())._obj._name ); + TypeMemPtr clztmp = TypeMemPtr.make( (byte) 2, clz, true ); + return parsePostfixName( con( clztmp ), id,0 ); + //return parsePostfixName(_scope.in(2),id); + } + } + + // Check for assign-update, x += e0; + char ch = _lexer.matchOperAssign(); + if( ch==0 ) { // Normal primary, check for postfix updates + int selfx = _scope.kindFcn(var)._lexSize; + // Might be a method call from inside a method, so no explicit "self". + // Pass the in-scope "self" + return parsePostfixMethod(rvalue,_scope.in(_scope.update(_scope.var(selfx),null))); + } + // Assign-update direct into Scope + if( var._final && !_scope.canInit(var) ) + throw error("Cannot reassign final '"+var._name+"'"); + checkSharedFunction(var); + Node op = opAssign(ch,rvalue, var.type() ); + _scope.update(var,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, Type t ) { + // 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); + case '|' -> new OrNode(null,lhs,rhs); + case '&' -> new AndNode(null,lhs,rhs); + case '^' -> new XorNode(loc(),lhs,rhs); + case Lexer.SHL -> new ShlNode(loc(),lhs,rhs); + case Lexer.SAR -> new SarNode(loc(),lhs,rhs); + case Lexer.SHR -> new ShrNode(loc(),lhs,rhs); + default -> throw TODO(); + }; + // Convert to float ops, or narrow int types; error if not declared type. + // Also, if postfix LHS is still keep() + return convertExpr(peep(op),t); + } + + + /** + Parse an allocation + */ + private Node alloc() { + Type t = type(); + if( t==null ) { + String tname = _lexer.matchId(); + if( tname==null || KEYWORDS.contains(tname) ) + throw error("Expected a type"); + t = forwardRefType(tname); + } + // Parse ary[ length_expr ] + if( match("[") ) { + if( !t.makeZero().isa(t) ) + throw error("Cannot allocate a non-nullable, since arrays are always zero/null filled"); + Node len = parseAsgn().keep(); + require("]"); + return allocArray(typeAry(t,false),len); + } else { + if( !(t instanceof TypeMemPtr tmp) ) + throw error("Cannot allocate a "+t.str()); + return allocStruct(tmp._obj); + } + } + + private Node allocStruct(TypeStruct ts) { + Ary<Node> ctorArgs = constructorArgs(); + // A field declaration can leave a short forward type name in TYPES. + // Allocation happens in lexical context, so resolve that short name + // to the completed nested type before selecting its class helper. + String typeName = ts._name.indexOf('.') == -1 ? fullTypeName(ts._name) : ts._name; + Type resolved = TYPES.get(typeName); + if( resolved instanceof TypeStruct rts ) ts = rts; + TypeStruct clz = (TypeStruct)TYPES.get(addClzPrefix(typeName)); + if( clz==null ) + throw error("Unknown struct type '" + ts._name + "'"); + Field afld = clz.field("<new>"); + Node alloc; + if( afld != null ) { + TypeFunPtr tfp = (TypeFunPtr)afld._t; + FunNode fun = _code.link(tfp); + alloc = fun==null + ? new FRefNode((addClzPrefix(typeName)+".<new>").intern(),loc()) + : new FunPtrNode(tfp,_code._start,fun.ret()).peephole(); + } else { + if( !ts._fref ) + throw error("Constructor arguments for '" + ts._name + "' but no constructor is defined"); + Type[] sig = new Type[ctorArgs._len]; + for( int i=0; i<ctorArgs._len; i++ ) + sig[i] = ctorArgs.at(i)._type; + alloc = new FRefNode((addClzPrefix(typeName)+".<new>").intern(),loc(), + TypeFunPtr.make(false,sig,TypeMemPtr.make(ts))); + } + Ary<Node> args = new Ary<>(Node.class); + args.add(ctrl()); args.add(mem()); + for( Node arg : ctorArgs ) args.add(arg.unkeep()); + args.add(alloc); + return functionCall(args); + } + + private Ary<Node> constructorArgs() { + Ary<Node> args = new Ary<>(Node.class); + if( !match("(") ) + return args; + while( !peek(')') ) { + Node arg = parseAsgn(); + if( arg==null ) break; + args.push(arg.keep()); + if( !match(",") ) break; + } + require(")"); + return args; + } + + private Node allocArray(TypeStruct ts, Node len) { + Field lenFld = ts.field("#" ); + Field bodyFld = ts.field("[]"); + + ConFldOffNode base = off(ts, "[]"); + int scale = ts.aryScale(); + Node size = peep(new AddNode(base,peep(new ShlNode(null,len,con(scale))))); + + // Build a NewNode; takes in and puts out all aliases. + NewNode nnn = new NewNode(ts, ctrl(), size ).init(); + ProjNode self = new ProjNode(nnn,0,ts.str()).init().keep(); + ProjNode smem = new ProjNode(nnn,1,"#selfMem").init().keep(); + + // Store length. Rest of array is zero'd via CALLOC during CodeGen. + // Length is casted to sanity. + // TODO: Needs runtime check. + Node st = peep(new StoreNode(null, "#", 1, TypeInteger.U32, null, smem, self, off(ts,"#"), len.unkeep(), true)); + + // TODO: Allow add-on "constructor" to init the array elements + + Node prior = mem(); + Node esc = peep(new EscapeNode(lenFld,self,st,prior)); + mem(mergeAlias(prior,lenFld._alias,esc)); + prior = mem(); + esc = peep(new EscapeNode(bodyFld,self,smem,prior)); + mem(mergeAlias(prior,bodyFld._alias,esc)); + smem.unkeep(); + return self.unkeep(); + } + + private Node newString(String s) { + TypeStruct ts = typeAry(TypeInteger.U8,true); + int lenAlias = ts.field("#" )._alias; + int elemAlias = ts.field("[]")._alias; + TypeConAryB body = TypeConAryB.make(s); + TypeInteger slen = TypeInteger.constant(s.length()); + // Make a TMP, not-null (byte)2, singleton (true) (requires text + // strings are hash-interned), with a TypeStruct having a constant + // array body. + TypeMemPtr str = TypeMemPtr.make((byte)2,TypeStruct.makeAry("[]u8", slen, lenAlias, body, elemAlias, true),true); + assert str.isConstant(); + return con(str.makeRO()); + } + + /** + * 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 parsePostfixMethod(Node expr, Node self) { + // TODO: This is too broad because the parser lacks strong types here. + // Some normal functions can end up with a "self" argument. + + // Self method call? Self is a TMP-not-CLZ, then assume and pass self. + if( match("(") ) { + expr = parsePostfix(require(functionCall(expr,self),")")); + } else if( expr != self ) + self.isKill(); // Not a method call, no need for self + return parsePostfix(expr); + } + private Node parsePostfix(Node expr) { + _lexer.skipWhiteSpace(); + Lexer site = loc(); + String name; + if( match(".") ) name = requireId(); + else if( match("#") ) name = "#"; + else if( match("[") ) name = "[]"; + // Can get here without an explicit receiver: "f()" or + // "self.method()(next)(next)(next)". Pass the current function self. + else if( match("(") ) return parsePostfix(require(functionCall(expr,defaultSelf()),")")); + else return expr; // No postfix + + return parsePostfixName(expr,name,0,site); + } + + private boolean privateSelf(Node ptr) { + while( ptr instanceof ConvertNode || ptr instanceof ReadOnlyNode || ptr instanceof GuardNode ) ptr=ptr.in(1); + Node self=defaultSelf(); + while( self instanceof ConvertNode || self instanceof ReadOnlyNode || self instanceof GuardNode ) self=self.in(1); + return ptr==self; + } + + // An explicit self.x read sees the same current value as bare x. Loading + // the allocation here would miss updates still held in local SSA values. + private Node privateField(String name, int pre) { + int idx=fieldVarIdxBefore(_scope._kinds.at(_scope.enclosingFuncOrDecl())._lexSize,name); + if( idx<0 ) throw error("Cannot resolve field '"+name+"'"); + Var v=_scope.var(idx); + if( pre==0 && matchOpx('=','=') ) { + checkSharedFunction(v); + Node value=parseAsgn().keep(); + _scope.update(v,convertExpr(value,v.type())); + return value.unkeep(); + } + Node value=_scope.in(_scope.update(v,null)); + if( v._uninit && missingInit(value,new BitSet()) ) + throw error("Field '"+name+"' might not be initialized",v._loc); + char ch=pre==0 ? _lexer.matchOperAssign() : (char)pre; + if( ch!=0 ) { + checkSharedFunction(v); + Node op=opAssign(ch,value,v.type()); + _scope.update(v,op); + if( pre!=0 && postfix(ch) ) value.unkeep(); + return pre==0 && postfix(ch) ? value.unkeep() : op; + } + return parsePostfixMethod(value,defaultSelf()); + } + + private void checkSharedFunction(Var v) { + if( _scope.canInit(v) && v._final && v.type() instanceof TypeFunPtr t && t.isConstant() ) + throw error("Cannot reassign shared function '"+v._name+"'"); + } + + // A loop Phi is temporarily bottom before its backedge is parsed. That is + // not an unwritten field: follow its live inputs to the actual defaults. + private boolean missingInit(Node value, BitSet seen) { + if( value==null || seen.get(value._nid) ) return false; + seen.set(value._nid); + if( value instanceof PhiNode phi && !(value instanceof ParmNode) ) { + for( int i=1; i<phi.nIns(); i++ ) + if( phi.region().in(i)!=null && phi.region().in(i)._type!=Type.XCONTROL && missingInit(phi.in(i),seen) ) + return true; + return false; + } + return !initialized(value._type); + } + + private Node defaultSelf() { + int selfx = _scope._kinds.at(_scope.enclosingFuncOrDecl())._lexSize; + return _scope.in(_scope.update(_scope.var(selfx),null)); + } + + /** + * Parse postfix expression; this can be a field expression, an array + * lookup or a postfix operator like '#' + * + * <pre> + * expr ['++' | '--' ] // Postfix unary write operator + * expr [ [op]= expr ] // Field reference + * expr ('[' expr ']')* [ [op]= expr ] // Array reference + * </pre> + */ + + // pre is +/-1 for a prefix update, otherwise parse a postfix operation. + private Node parsePostfixName(Node expr, String name, int pre) { + return parsePostfixName(expr,name,pre,loc()); + } + + private Node parsePostfixName(Node expr, String name, int pre, Lexer site) { + if( inExplicitConstructor() && privateSelf(expr) ) return privateField(name,pre); + // Keep expr across possible updates + expr.keep(); + + // Get the best known TypeStruct from a pointer type. + TypeStruct ts = null, ts2; + if( expr._type instanceof TypeMemPtr tmp && (ts2=((TypeStruct)TYPES.get(tmp._obj._name))) != null ) + ts = (TypeStruct)tmp._obj.join(ts2); + // A Load's flow type is intentionally not promised during parsing, + // but its declaring field type is still known. Recover that static + // type for chained accesses such as `_grow(1).buf[len++]`; otherwise + // the array Store is parsed as alias #1 and can hide the independent + // side effect of the postfix `len++`. + if( ts==null && expr instanceof LoadNode load ) { + TypeStruct ots = load.off() instanceof ConFldOffNode coff ? coff._ts : null; + if( ots==null && load.ptr()._type instanceof TypeMemPtr owner ) { + Type owner0 = TYPES.get(owner._obj._name); + ots = owner0 instanceof TypeStruct obj ? obj : owner._obj; + } + if( ots != null ) { + Field loaded = ots.field(load._name); + if( loaded != null && loaded._t instanceof TypeMemPtr ltmp ) { + Type loaded0 = TYPES.get(ltmp._obj._name); + ts = loaded0 instanceof TypeStruct obj ? obj : ltmp._obj; + } + } + } + + Field fld = ts==null ? null : ts.field(name); + if( fld==null && ts!=null && !startsClzPrefix(ts._name) ) { + Type clz0 = TYPES.get(addClzPrefix(ts._name)); + if( clz0 instanceof TypeStruct clz && (fld = latestStruct(clz).field(name)) != null ) { + expr.unkeep(); + ts = latestStruct(clz); + expr = con(TypeMemPtr.make((byte)2,ts,true)).keep(); + } + } + if( fld==null && ts!=null ) + fld = fieldOrOpen(ts,name,false); + if( fld==null && ts!=null && !latestStruct(ts)._open ) + throw error("Accessing unknown field '"+name+"' from '*"+ts._name+"'"); + + // We have to record "active touches" of a new class, to track when we + // need to force class initialization. Basically, we insert an CLInit, + // and if it does not fold away it'll turn into a runtime check. + Kind.Func func = (Kind.Func)_scope._kinds.at(_scope.enclosingFuncOrDecl()); + if( (ts==null || ts==TypeStruct.BOT || ts.isHigh() || startsClzPrefix(ts._name)) && + !(ts!=null && (ts._name+".<clinit>").equals(func._name)) ) + classInit(site,expr); + + // With no field, we will update the bulk memory + int alias = fld==null ? 1 : fld._alias; + Type decl = fld==null ? Type.BOTTOM : fld._t; + + // Field offset; fixed for structs, computed for arrays + Node off = (name=="[]" + // Array element math, with unknown base offset and shift amount. + ? peep(new AddNode(fldoff(expr,"[]"),peep(new ShlNode(null,require(parseAsgn(),"]"),fldoff(expr,"<<"))))) + // Named field offset on an unknown struct type + : fldoff(expr,name)).keep(); + + // Disambiguate "obj.fld==x" boolean test from "obj.fld=x" field assignment + if( pre==0 && matchOpx('=','=') ) { + // Field assignment + Node val = parseAsgn().keep(); + // Lift value for store + Node lift = fld==null ? val : new ConvertNode(fld._t,val).peephole(); + // Memory for store, post assignment expression + Node mem = mem(); + // Store to field + mem.keep(); + if( expr._type instanceof TypeMemPtr view && view._ro && ctrl()._type!=Type.XCONTROL ) + throw error("Cannot modify final field '"+name+"'"); + Node st = new StoreNode(loc(), name, alias, decl, ctrl(), mem, expr.unkeep(), off.unkeep(), lift, false).peephole().keep(); + storeMem(st,alias,mem); + st.unkeep(); + mem.unkeep(); + + return val.unkeep(); // "obj.a = expr" returns the expression while updating memory + } + + // Memory for load + Node mem = mem(); + // Loading from a constant array, the declared type is the + // meet-over-elements and not the array itself. + if( name=="[]" && decl instanceof TypeConAry conary ) + decl = conary.elem(); + + // Load field + Node load = peep(new LoadNode(loc(),name, alias, ctrl(), mem, expr, off)); + + // Check for assign-update, "ptr.fld += expr" or "ary[idx]++" + char ch = pre==0 ? _lexer.matchOperAssign() : (char)pre; + if( ch!=0 ) { + if( fld!=null && decl==Type.BOTTOM ) + throw error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor" ); + Node op = opAssign(ch,load, decl ).keep(); + mem.keep(); + if( expr._type instanceof TypeMemPtr view && view._ro && ctrl()._type!=Type.XCONTROL ) + throw error("Cannot modify final field '"+name+"'"); + Node st = new StoreNode(loc(), name, alias, decl, ctrl(), mem, expr.unkeep(), off.unkeep(), op, false).peephole().keep(); + storeMem(st,alias,mem); + st.unkeep(); + mem.unkeep(); + + op.unkeep(); // A narrow Store may have folded away the expression's conversion. + if( postfix(ch) ) load.unkeep(); + return pre==0 && postfix(ch) ? load : op; + } + off.unkill(); + // Might be a method call, so pass expr as 'self' + return parsePostfixMethod(load,expr.unkeep()); + } + + + // 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 TypeFloat && t instanceof TypeInteger ) + val = new ToIntegerNode(val).peephole(); + 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); + java.util.BitSet finals = new java.util.BitSet(); + Type self = implicitSelfType(); + ts.push(self); + ids.push(self == TypePtr.PTR ? "#self" : "self"); + + // Parse other arguments + _lexer.skipWhiteSpace(); + Lexer loc = loc(); // First argument location + while( true ) { + Type t = type(); // Arg type + if( t==null ) break; + finals.set(ids.size(),bindingFinal(t)); + 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 = TypeFunPtr.make1((byte)2,true,ts.asAry(),Type.BOTTOM,_code.fidx(_ref._cname)); + ReturnNode ret = parseFunctionBody(tfp,loc,finals,ids.asAry()); + return new FunPtrNode(tfp,_code._start,ret).peephole(); + } + + private Node constructorFunc(String name) { + require("{"); + Ary<Type> ts = new Ary<>(Type.class); + Ary<String> ids = new Ary<>(String.class); + java.util.BitSet finals = new java.util.BitSet(); + TypeStruct self = _scope.constructorSelf(); + assert self != null; + ts.push(TypeMemPtr.make(constructorRecv(self))); + ids.push("self"); + // Explicit constructors use the same hidden allocation arguments as + // <init>: public memory is the call memory edge (index 1), followed by + // self (index 2) and the private, unescaped object memory (index 3). + ts.push(TypeMem.makePrivate(self)); + ids.push("#selfMem"); + + _lexer.skipWhiteSpace(); + Lexer loc = loc(); + while( true ) { + Type t = type(); + if( t==null ) break; + finals.set(ids.size(),bindingFinal(t)); + String id = requireId(); + ts .push(t ); + ids.push(id); + match(","); + } + require("->"); + TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),TypeMem.makePrivate(self),_code.fidx(_ref._cname)); + Node[] preParse = _scope.save(); + ReturnNode ret = _parseFunctionBody((self._name+"."+name).intern(),tfp,loc,finals,ids.asAry()); + _scope.restore(preParse); + require("}"); + return new FunPtrNode(tfp,_code._start,ret).peephole(); + } + + /** + * Parse function call arguments; caller will parse the surrounding "()" + * <pre> + * ( arg* ) + * </pre> + */ + private Node functionCall(Node fcn, Node self) { + Lexer site = loc(); + site._position--; // Opening '('; arguments can contain nested calls. + if( fcn._type == Type.NIL ) + throw error("Calling a null function pointer"); + fcn.keep(); // Keep while parsing args + + Ary<Node> args = new Ary<>( Node.class ); + args.push(null); // Space for ctrl,mem + args.push(null); + if( self != null ) // Method call has a self + args.push(self.keep()); + 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,mem().keep()); + args.push(fcn); // Function pointer + // Unkeep them all + for( Node arg : args ) + arg.unkeep(); + return functionCall(args,site); + } + + private Node functionCall( Ary<Node> args ) { + return functionCall(args,loc()); + } + + private Node functionCall( Ary<Node> args, Lexer site ) { + // 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(site, args.asAry()).peephole(); + // Post-call setup + CallEndNode cend = (CallEndNode)new CallEndNode(call,TypeRPC.constant(_code.rpc(_ref._cname))).peephole(); + call.peephole(); // Rerun peeps after CallEnd, allows early inlining + cend = (CallEndNode)cend.keep().peephole(); // TODO: Might inline + // Control from CallEnd + ctrl(new CProjNode(cend,0,ScopeNode.CTRL).peephole()); + // Memory from CallEnd + Node mem = new ProjNode(cend,1,ScopeNode.MEM0).peephole(); + mem(mem); + // Call result + return new ProjNode(cend.unkeep(),2,"#2").peephole(); + } + + // Just after parsing "type foo = " can parse `"C"` + private boolean isExternDecl( ) { + int old = pos(); + String s = parseString(); + if( "C".equals(s) ) return true; + pos(old); + return false; + } + // External linked constant + private ConstantNode externDecl( String ex, Type t ) { + if( t instanceof TypeFunPtr tfp ) { // Generic TFP from type parse + int fidx = _code.fidx(_ref._cname); // Get a FIDX + tfp = tfp.makeFrom(fidx); // Become a constant + _code.externFunc(fidx,ex); // Map fidx to extern name + t = tfp; + } + return (ExternNode)(new ExternNode(t,ex).peephole()); + } + + // A lexical C binding has a singleton namespace with one external field. + // The namespace occupies no storage; its field offset supplies the address. + private Node externData(String name, Type t) { + if( !(t instanceof TypeInteger) && !(t instanceof TypeFloat) ) + throw error("C data binding requires an integer or float type"); + String cname = ("C:"+name).intern(); + TypeStruct ts = TypeStruct.make(cname,false,Field.make(name,t,_code.externAlias(name),false,true)); + Type old = TYPES.putIfAbsent(cname,ts); + if( old!=null && old!=ts ) + throw error("Conflicting types for C data '"+name+"'"); + return con(TypeMemPtr.make((byte)2,ts,true)); + } + + /** + * Parse integer literal + * + * <pre> + * integerLiteral: [1-9][0-9]* | [0] + * floatLiteral: [digits].[digits]?[e [digits]]?[f]? + * </pre> + */ + private ConstantNode parseLiteral() { return con(_lexer.parseNumber()); } + ConstantNode con( long con ) { return _code.con(con); } + ConstantNode con( Type t ) { return _code.con(t ); } + private void classInit(Lexer site, Node ptr) { + // Ordinary load machinery resolves the flag alias and offset when + // the receiver is still a forward reference. + Field flag = ptr._type instanceof TypeMemPtr tmp ? tmp._obj.field("$init") : null; + int alias = flag==null ? 1 : flag._alias; + Node ready = new LoadNode(site,"$init",alias,null,mem(),ptr,fldoff(ptr,"$init")).peephole(); + CLInitNode init = new CLInitNode(site,ctrl(),mem(),ptr,ready).peephole().keep(); + ctrl(new CProjNode(init,0,ScopeNode.CTRL).peephole()); + Node bulk = new ProjNode(init,1,ScopeNode.MEM0).peephole().keep(); + flag = init.flag(); + mem(flag==null ? bulk : mergeAlias(bulk,flag._alias, + new ProjNode(init,2,memName(flag._alias)).peephole())); + bulk.unkeep(); + init.unkeep(); + } + + // Field offset for a known type (but unknown offset) + public static ConFldOffNode off( TypeStruct ts, String fname ) { + return (ConFldOffNode)(new ConFldOffNode(ts,fname).peephole()); + } + // Field offset for an unknown type + public Node fldoff( Node n, String name ) { + return new FldOffNode(n,name,loc()).peephole(); + } + public Node peep( Node n ) { + // Peephole, then improve with lexically scoped guards + return _scope.upcastGuard(n.peephole()); + } + + // Parse a string or null + private String parseString() { + if( !peek('"') ) return null; + _lexer.inc(); + StringBuilder sb = new StringBuilder(); + while( !_lexer.isEOF() ) { + char c = _lexer.nextChar(); + if( c=='"' ) return sb.toString(); + if( c!='\\' ) { + sb.append(c); + continue; + } + if( _lexer.isEOF() ) + throw error("Unclosed string"); + sb.append(escapedChar("string")); + } + throw error("Unclosed string"); + } + + private char escapedChar(String kind) { + char esc = _lexer.nextChar(); + return switch( esc ) { + case '0' -> '\u0000'; + case 'n' -> '\n'; + case 't' -> '\t'; + case 'r' -> '\r'; + case '\\' -> '\\'; + case '"' -> '"'; + case '\'' -> '\''; + default -> throw error("Unknown "+kind+" escape \\\\"+esc); + }; + } + + // Already parsed "'" + private Node parseChar() { + if( _lexer.isEOF() ) throw error("Unclosed character"); + char c = _lexer.nextChar(); + if( c=='\\' ) { + if( _lexer.isEOF() ) throw error("Unclosed character"); + c = escapedChar("character"); + } + return require(con(TypeInteger.constant(c)),"'"); + } + + private boolean noLowerCase(String s) { + for( int i=0; i<s.length(); i++ ) + if( Character.isLowerCase(s.charAt(i)) ) + return false; + return true; + } + + + ////////////////////////////////// + // 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 matchOp(char c0 ) { return _lexer.matchOp(c0); } + 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; + public final CompUnit _cu; // Original source, retained across inlining. + // Tracks current position in input buffer + private int _position = 0; + + // + private int _line_number = 1; + // Start of current line + private int _line_start = 0; + + /** True when this location precedes {@code that} in the same source. */ + public boolean before(Lexer that) { + return that != null && (_cu!=null ? _cu==that._cu : _input==that._input) && _position < that._position; + } + + public String position() { return _line_number+":"+(_position-_line_start+1); } + public int pos() { return _position; } + + /** + * Record the source text for lexing + */ + public Lexer(String source) { + this(source.getBytes()); + } + + /** + * Direct from disk file source + */ + public Lexer(byte[] buf) { + _input = buf; + _cu = null; + } + + private Lexer(CompUnit cu) { + _input = cu._src.getBytes(); + _cu = cu; + } + + // Imported location: source text is only needed when reporting an error. + private Lexer(CompUnit cu, int pos) { + _input = null; + _cu = cu; + _position = pos; + } + + + /** + * Copy a lexer from a lexer + */ + private Lexer(Lexer l) { + _input = l._input; + _cu = l._cu; + _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 _input==null ? _cu._cname+"@"+_position : 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 if( _position+2 < _input.length && + _input[_position ] == '/' && + _input[_position+1] == '*') { + // Skip /*comment*/ + while( !isEOF() && !(_input[_position-1] == '*' && _input[_position] == '/')) + inc(); + 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; + } + // Match this char, and the next char must be different. + // Handles '&&' vs '&' + boolean matchOp( char c0 ) { + skipWhiteSpace(); + if( _position+1 >= _input.length || _input[_position]!=c0 || _input[_position+1]==c0 ) + return false; + inc(); + return true; + } + // Match these two characters in a row + 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 "EOF"; + 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(); + boolean f32 = peek()=='f'; + if( f32 ) nextChar(); + int n = Math.abs(len); + if( f32 ) + return TypeFloat.constant(Float.parseFloat(new String(_input,old,n))); + 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,n))); + } + private String parseNumberString() { + int old = _position; + int len = Math.abs(isLongOrDouble()); + if( peek()=='f' ) { + nextChar(); + len++; + } + 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; + if( peek() == '-' ) nextChar(); + 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).intern(); + } + + // + 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) + // Distinct tags for the multi-character compound operators. + static final char SHL = 2, SAR = 3, SHR = 4; + + public char matchOperAssign() { + skipWhiteSpace(); + if( match("<<=" ) ) return SHL; + if( match(">>>=") ) return SHR; + if( match(">>=" ) ) return SAR; + if( _position+2 >= _input.length ) return 0; + char ch0 = (char)_input[_position]; + if( "+-/*&|^".indexOf(ch0) == -1 ) return 0; + char ch1 = (char)_input[_position+1]; + if( ch1 == '=' ) { _position += 2; return ch0; } + if( isIdLetter((char)_input[_position+2]) ) return 0; + if( ch0 == '+' && ch1 == '+' ) { _position += 2; return (char) 1; } + if( ch0 == '-' && ch1 == '-' ) { _position += 2; return (char)-1; } + return 0; + } + } + + ParseException errorSyntax(String syntax) { return _errorSyntax("expected `"+syntax+"`"); } + private ParseException _errorSyntax(String msg) { + return error("Syntax error, "+msg+" but found `" + _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; } + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/Var.java b/chapter-modules/src/main/java/com/seaofnodes/simple/Var.java new file mode 100644 index 00000000..af371d21 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/Var.java @@ -0,0 +1,54 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + + +/** + * The tracked fields are now complex enough to deserve a array-of-structs layout + */ +public class Var { + + public final String _name; // Declared name + // These fields are not final for forward reference late updates or + // promotions. + public int _idx; // index in containing scope + private Type _type; // Declared type + public boolean _final; // Final field + public boolean _fref; // Forward ref + // Parser-only definite-initialization marker. The current binding's + // nullable type carries the path-sensitive state through SSA merges. + public boolean _uninit; + public int _extern; // C storage alias; zero for ordinary variables + public Parser.Lexer _loc; // Source location + + 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; + _fref = fref; + _loc = loc; + } + public Type type() { return _type; } + public Type _type() { return _type; } // ScopeNode printer needs to have no side effects + + // 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; + _fref = false; + _loc = loc; + } + + @Override public String toString() { + return (_fref ? "FREF " : "")+(_uninit ? "UNINIT " : "")+_type.toString()+(_final ? " ": " !")+_name; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java new file mode 100644 index 00000000..0b314978 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java @@ -0,0 +1,101 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; + +abstract public class BuildLRG { + // Compute live ranges in a single forwards pass. Every def is a new live + // range except live ranges are joined at Phi. Also, gather the + // intersection of register masks in each live range. Example: "free + // always zero" register forces register zero, and calling conventions + // typically force registers. + + // Sets FAILED to the set of hard-conflicts (means no need for an IFG, + // since it will not color, just split the conflicted ranges now). Returns + // true if no hard-conflicts, although we still might not color. + public static boolean run(int round, RegAlloc alloc) { + FunNode fun = null; + for( Node bb : alloc._code._cfg ) { + if( bb instanceof FunNode fun2 ) + fun = fun2; + for( Node n : bb.outs() ) { + // A synthetic never-exit value has no runtime definition. + if( n._type==Type.TOP ) continue; + if( n instanceof PhiNode phi && !(phi._type instanceof TypeMem) ) { + if( phi instanceof MachNode mach && mach.outregmap()==null ) + continue; + // All real Phi inputs end up with the same LRG. + // Pass 1: find any pre-existing LRG, to avoid make-then-Union a LRG + LRG lrg = alloc.lrg(phi); + if( lrg == null ) + for( int i=1; i<phi.nIns(); i++ ) + if( (lrg = alloc.lrg(phi.in(i))) != null ) + break; + // If none, make one. + if( lrg==null ) lrg = alloc.newLRG(n,fun); + if( phi instanceof MachNode ) defLRG(alloc,n,fun); + // Pass 2: everybody uses the same LRG + lrg=alloc.union(lrg,phi); + for( int i=phi instanceof ParmNode ? 2 : 1; i<n.nIns(); i++ ) + // TOP keeps the type merge neutral, but needs no register. + if( n.in(i)._type!=Type.TOP ) + lrg=alloc.union(lrg,n.in(i)); + if( lrg._mask!=null && lrg._mask.isEmpty() ) + alloc.fail(lrg); + + } else if( n instanceof MachNode mach ) { + // Attempt to commute ops to keep live ranges compatible. + if( mach.commutes() && n.nOuts()==1 ) { + int uidx = n.out(0)._inputs.find(n); + RegMask mask1 = n.in (1) instanceof MachNode machx ? machx.outregmap() : alloc.lrg(n.in(1))._mask; + RegMask mask2 = n.in (2) instanceof MachNode machx ? machx.outregmap() : alloc.lrg(n.in(2))._mask; + RegMask masko = n.out(0) instanceof MachNode machx ? machx.regmap(uidx): mach.outregmap(); + if( masko!=null && !mask1.overlap(masko) && mask2.overlap(masko) ) + n.swap12(); + } + + // Define live range + defLRG(alloc,n,fun); + + // Now, look in the opposite direction. How are incoming + // LRGs affected by this node: For all uses, make live lrgs + for( int i=1; i<n.nIns(); i++ ) + if( n.in(i)!=null ) { + LRG lrg2 = alloc.lrg(n.in(i)); + if( lrg2 != null ) { // Anti-dep or other, no LRG + RegMask use_mask = mach.regmap(i); // use_mask is also null for anti-dep + if( use_mask!=null && !lrg2.machUse(mach,(short)i,use_mask.size1()).and(use_mask) ) + alloc.fail(lrg2); // Empty register mask, must split + } + } + + } + + // MultiNodes have projections which set registers + if( n instanceof MultiNode && !(n instanceof CFGNode) ) + for( Node proj : n.outs() ) + if( proj instanceof MachNode ) + defLRG(alloc,proj,fun); + } + } + + // Collect live ranges + alloc.unify(); + + return alloc.success(); + } + + private static void defLRG( RegAlloc alloc, Node n, FunNode fun ) { + MachNode mach = (MachNode)n; + RegMask def_mask = mach.outregmap(); + if( def_mask == null ) return; + LRG lrg = mach.twoAddress() == 0 + ? alloc.newLRG(n,fun) // Define a new LRG for N + : alloc.lrg2(n,mach.twoAddress()); // Use the matching 2-adr input + // Record mask and mach + if( !lrg.machDef(mach,def_mask.size1()).and(def_mask) ) + alloc.fail(lrg); // Empty register mask, must split + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java new file mode 100644 index 00000000..6c70b71d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java @@ -0,0 +1,214 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.util.*; + +// Search for Class Initialization Cycles. + +// Done after Opto, so dead code does not make a cycle. + +// Build the inits-graph, a call-graph plus CLInits plus class parents. + +// (1) Scan all functions f, building _edges[f][]: the set of <cinit> fcns from +// CompUnits from CLInitNodes, and the set of fcn call targets in f and the +// class parent. These are all reported as fidxs (ints). +// +// (2) Walk the _edges graph using Tarjans SCC. Reject cycles containing more +// than one <clinit>. +// +// (3) Report on <clinit> cycles +// +public class ClassInitDependencies { + private final CodeGen _code; + // Function FIDX -> callees, touched initializers, and parent initializer. + public final int[][] _edgess; + private final int[] _pres; + private final int[] _lows; + private final int[] _stk ; + private int _sp; + + + // Build once; tests can inspect the graph independently of cycle checking. + public ClassInitDependencies( CodeGen code ) { + _code = code; + assert code._phase.ordinal() >= CodeGen.Phase.IterPostOpto.ordinal(); + _pres = new int[code._linker._len]; + _lows = new int[code._linker._len]; + _stk = new int[code._linker._len]; + + // + BitSet visit = code.visit(); // Visit set per-fcn + Ary<CFGNode> work = new Ary<>(CFGNode.class); // Worklist per-fcn + IntHashMap iset = new IntHashMap(0); // dedup hash set per-fcn + _edgess = new int[code._linker._len][]; + + // For all functions, build the _edges array + for( FunNode fun : code._linker ) + if( fun!=null && !fun.isDead() ) + // Visit function and collect direct class touches and callees + _edgess[fun.sig().fidx()] = walk(code,fun,work,iset,visit,null); + visit.clear(); + } + + // Return the outgoing init edge set for this function + private static int[] walk( CodeGen code, FunNode fun, Ary<CFGNode> work, IntHashMap iset, BitSet visit, + Ary<Parser.Lexer> sites) { + // Walk function body, looking for outgoing init edges + assert iset .isEmpty(); // int hash set for de-dup + assert visit.isEmpty(); // needs a visit set, because loops + assert work .isEmpty(); // needs a worklist, because DAG, parallel explore paths + + // Children classes always dependent on parents + if( fun.isClz() && fun._compunit._par != null ) + iset.put(code.link(fun._compunit._par).sig().fidx(),1); + + work.push(fun); + while( !work.isEmpty() ) { + CFGNode cfg = work.pop(); + if( visit.get(cfg._nid) ) continue; // Been there, done that + visit.set(cfg._nid); + assert !cfg._type.isHigh(); + // Do not walk off end of function + if( cfg instanceof ReturnNode ) continue; + + // CLInit adds a class init requirement to this function + // (plus all parent classes) + if( cfg instanceof CLInitNode init ) { + FunNode clz = code.link(init.cu()); // <clinit> referred to + iset.put(clz.sig().fidx(),1); // Add to edge set + if( sites!=null && sites.atX(clz.sig().fidx())==null ) sites.setX(clz.sig().fidx(),init._loc); + } + + // Calls union all their fptr fidxs into the callee set + if( cfg instanceof CallNode call && call.fptr()._type instanceof TypeFunPtr fp ) { + assert !XInt.isHigh(fp.fidxs()) && fp.nfcns()>0; // Call is alive with valid targets + // Union call targets into the callee set + for( int dstx = XInt.next(fp.fidxs(),0); dstx>=0; dstx=XInt.next(fp.fidxs(),dstx) ) { + iset.put(dstx,1); + if( sites!=null && sites.atX(dstx)==null ) sites.setX(dstx,call._loc); + } + } + + // Forward recursion to walk the whole function + for( Node use : cfg._outputs ) { + // Do not walk into another function + if( !(use instanceof CFGNode cfg1) || use instanceof FunNode || use instanceof StopNode ) continue; + // Expect control flow edge is sensible + assert (use instanceof RegionNode r && r._inputs.find(cfg)>0) || use.in(0)==cfg; + if( !visit.get(cfg1._nid) ) // Small speed opt + work.add(cfg1); + } + } + + visit.clear(); + int[] edges = iset.isEmpty() ? null : iset.keySet(); + iset.clear(); + return edges; + } + + // Return an error string, or null if no cycle contains multiple clinits. + public String check() { + Arrays.fill(_pres,0); + _sp = 0; + _pres[0] = 1; // Preorders stored in pres[0] and start at 1 + + int idx = -1; + for( int fidx = 1; fidx < _edgess.length; fidx++ ) + if( _pres[fidx] == 0 && // Already visited + (idx = scc(fidx)) != -1 ) // And found a cycle + return report(idx); // Report cycle + return null; + } + + // Every internal edge of an SCC belongs to a cycle. Print all of them: + // stack order is not a path, and enumerating all cycle paths can explode. + // This bookkeeping and formatting are only needed on an error. + private String report(int idx) { + BitSet members = new BitSet(); + for( int i=idx; i<_sp; i++ ) members.set(_stk[i]); + SB sb = new SB().p("Cyclic class initialization:"); + // Recover one surviving source site per edge only after finding an + // error. A representative touch is enough; runtime first-touch order + // need not be statically known. Lexer snapshots retain the source CU + // even when the node has been inlined into a different function. + Ary<CFGNode> work = new Ary<>(CFGNode.class); + IntHashMap iset = new IntHashMap(0); + BitSet visit = new BitSet(); + Ary<Parser.Lexer> sites = new Ary<>(Parser.Lexer.class); + for( int i=idx; i<_sp; i++ ) { + int src = _stk[i]; + FunNode fun = _code.link(src); + Arrays.fill(sites._es,null); + sites.clear(); + walk(_code,fun,work,iset,visit,sites); + if( _edgess[src]!=null ) + for( int dst : _edgess[src] ) + if( members.get(dst) ) { + sb.p("\n ").p(name(src)).p(" -> ").p(name(dst)); + Parser.Lexer loc = sites.atX(dst); + if( loc!=null && loc._cu!=null ) + sb.p('\n').p(SourceError.format(loc._cu,loc.pos(),"cyclic class initialization")); + else if( fun.isClz() && fun._compunit._par==_code.link(dst)._compunit ) + sb.p("\n parent initialization requirement"); + } + } + return sb.toString(); + } + + private String name(int fidx) { + FunNode fun = _code.link(fidx); + String owner=fun._compunit._cname; + if( fun.isClz() ) return owner; + String label=fun.label(); + return (label.startsWith(owner+".") ? label : owner+"."+label)+" [fidx "+fidx+"]"; + } + + // Tarjan SCC + int scc( int fidx ) { + // Assign pre-order number + _lows[fidx] = _pres[fidx] = _pres[0]++; + _stk[_sp++] = fidx; + // walk outbound edges + if( _edgess[fidx]!=null ) { + for( int edge : _edgess[fidx] ) { + if( edge >= _pres.length ) continue; // Out of bounds happens when calling C fcns + if( _pres[edge] == 0 ) { // Not visited + int idx = scc(edge); + if( idx != -1 ) return idx; // Found a cycle + _lows[fidx] = Math.min(_lows[fidx],_lows[edge]); + } else if( _edgess[edge]!=null && onStk(edge) ) { + _lows[fidx] = Math.min(_lows[fidx],_pres[edge]); + } + } + } + // Found a cycle + if( _lows[fidx] == _pres[fidx] ) { + int idx = find(fidx); + if( hasClzCycle(idx) ) + return idx; // Found a class cycle + // Pop stack, including fidx + _sp = idx; + } + return -1; // No cycle + } + + private int find(int e) { + for( int i=0; i<_sp; i++ ) if( _stk[i] == e ) return i; + return -1; + } + private boolean onStk(int e) { return find(e) != -1; } + + private boolean hasClzCycle(int idx) { + int clinits = 0; + for( int i=idx; i<_sp; i++ ) { + FunNode fun = _code.link(_stk[i]); + if( fun!=null && fun.isClz() && ++clinits > 1 ) + return true; + } + return false; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java new file mode 100644 index 00000000..689ceceb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java @@ -0,0 +1,105 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +import java.util.BitSet; +import java.util.IdentityHashMap; + +// Coalesce copies +abstract public class Coalesce { + + public static boolean coalesce( int round, RegAlloc alloc ) { + + // Walk all the splits, looking for coalesce chances + boolean progress = false; + for( CFGNode bb : alloc._code._cfg ) { + outer: + for( int j=0; j<bb.nOuts(); j++ ) { + Node n = bb.out(j); + if( !(n instanceof SplitNode) ) continue; + LRG v1 = alloc.lrg(n); + LRG v2 = alloc.lrg(n.in(1)); + if( v1 != v2 ) { + LRG ov1 = v1, ov2 = v2; + // Get the smaller neighbor count in v1 + if( v1.nadj() > v2.nadj() ) + { v1 = v2; v2 = alloc.lrg(n); } + int v2len = v2._adj==null ? 0 : v2._adj._len; + // See that they do not conflict (coalescing would make a self-conflict) + if( v1._adj!=null && v2._adj!=null && v1._mask.overlap(v2._mask) ) { + // Walk the smaller neighbors, and add to the larger. + // Check for direct conflicts along the way. + for( LRG v3 : v1._adj ) { + if( v3==v2 ) { + // Direct conflict, skip this LRG pair + v2._adj.setLen(v2len); + continue outer; + } + // Add missing LRGs from V1 to V2 + if( v2._adj.find(v3)== -1 ) + v2._adj.push(v3); + } + } + // Most constrained mask + RegMask mask = v1._mask==v2._mask ? v1._mask : v1._mask.copy().and(v2._mask); + // Check for capacity + if( v2.nadj() >= mask.size() ) { + // Fails capacity, will not be trivial colorable + if( v2._adj!=null ) v2._adj.setLen(v2len); + continue; + } + + // Union lrgs. + progress = true; + + // Since `n` is dying, remove from the LRGS BEFORE doing + // the union; this will let the union code keep the + // matching bits from the other side, preserving some + // info for biased coloring + if( ov1. _machDef==n ) ov1. _machDef = null; + if( ov1._splitDef==n ) ov1._splitDef = null; + if( ov2. _machUse==n ) ov2. _machUse = null; + if( ov2._splitUse==n ) ov2._splitUse = null; + // Keep larger adjacency list. + Ary<LRG> larger = v2._adj; + v2._adj = null; + + // Union lrgs. Swaps again, based on small lrg + LRG vWin = v1.union(v2); + vWin._adj = larger; + LRG vLose = v1.leader() ? v2 : v1; + + // LRGs below v2len were always in v2, but maybe not in v1. + // LRGs above v2len are in v1 but were not in v2. + + // Walk v2's neighbors, replacing vLose with vWin. + if( larger != null ) + for( LRG vn : larger ) { + int idx = vn._adj.find(vLose); + if( idx != -1 ) + vn._adj.del(idx); + if( vn._adj.find(vWin) == -1 ) + vn._adj.add(vWin); + } + } else { + + // Since `n` is dying, remove from the LRGS + if( v1. _machDef==n ) v1. _machDef=null; + if( v1. _machUse==n ) v1. _machUse=null; + if( v1._splitDef==n ) v1._splitDef=null; + if( v1._splitUse==n ) v1._splitUse=null; + } + + // Remove junk split + n.removeSplit(); + j--; + } + } + + if( progress ) + alloc.unify(); + + return true; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java new file mode 100644 index 00000000..e6bace8c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java @@ -0,0 +1,625 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.graph.GraphObserver; +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.print.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.io.File; +import java.util.*; + + +// Compile Driver for a Compilation Unit +@SuppressWarnings("unchecked") +public class CodeGen { + + // Last created CodeGen as a global; used all over to avoid passing about a "context". + public static CodeGen CODE; + + // Location of built-in CPU ports + public static final String PORTS = "com.seaofnodes.simple.node.cpus"; + + // Compile phases + public enum Phase { + Parse, // Parse ASCII text into Sea-of-Nodes IR + Iter, // Pessimistic peepholes after parse + Opto, // SCCP, numeric modes, seed the peephole worklist + IterPostOpto, // Peepholes and inlining using SCCP results + FinalizeIR, // Field layout, offset cleanup, function/CU roots + TypeCheck, // Last check for bad programs + Lower, // Expand checked class initialization to ordinary IR + LoopTree, // Build a loop tree; break infinite loops + Select, // Convert to target hardware nodes + Schedule, // Global schedule (code motion) nodes + LocalSched, // Local schedule + RegAlloc, // Register allocation + Encoding, // Encoding + Export, // Export + LastPhase // After last phase + } + public Phase _phase; + // Null for normal compilation; owned by this compilation, not a global viewer. + public GraphObserver<Node> _obs; + + /** True when tests deliberately randomize Iter worklist order. */ + public static boolean iterSeedOverridden() { return System.getProperty("simple.iter.seed") != null; } + + // Expensive assert sampling: + // -Dsimple.assert.expensive=0 disables these checks + // -Dsimple.assert.expensive=1 checks every step + // -Dsimple.assert.expensive=N checks once every 1<<N steps + private static final int EXPENSIVE_ASSERT_LOG = Integer.getInteger("simple.assert.expensive",8); + public static boolean expensiveAssert() { return EXPENSIVE_ASSERT_LOG > 0; } + public static boolean expensiveAssert( int trip ) { + if( EXPENSIVE_ASSERT_LOG <= 0 ) return false; + if( EXPENSIVE_ASSERT_LOG == 1 ) return true; + int shift = Math.min(EXPENSIVE_ASSERT_LOG,30); + return (trip & ((1<<shift)-1)) == 0; + } + + // --------------------------- + // Module Source Root + public final String _modDir; + // Module Build Root + public final String _buildDir; + // Slashed path from source root to the entry file, without ".smp". + public final String _srcName; + + // Only available for tests, otherwise source comes from the CompUnit + private final String _src; + + // Current Working Directory; default module base + private final String _cwd; + + // Source files discovered in this invocation. All live initializers + // participate in cycle checking, including fully inlined bodies. + public HashMap<String,CompUnit> _compunits; + + // Test shortcut for only one compilation unit + public CompUnit compunit() { + assert _compunits.size()==1; + for( CompUnit cu : _compunits.values() ) + return cu; + assert false : "no compilation unit"; + return null; + } + + // True when fidx is published through a public class or instance field. + public boolean publicFIDX(int fidx) { + if( fidx < LocalIds.RESERVED ) return false; + // A public class initializer is itself an external root; unlike + // constructors and methods, it is not reached through a class field. + FunNode fun = _linker.atX(fidx); + if( fun != null && fun.isClz() && fun.isPublic() ) + return true; + // Include instance methods even while their constant function fields + // are stored in each instance rather than on the class object. + for( Type t : Parser.TYPES.values() ) + if( t instanceof TypeStruct clz && + Parser.startsClzPrefix(clz._name) && + Parser.isPublicClassName(clz._name) && + resolvedClass(clz) ) { + if( publicFieldFIDX(clz,fidx) ) return true; + Type self = Parser.TYPES.get(clz._name.substring(Parser.CLZ.length())); + if( self instanceof TypeStruct ts && publicFieldFIDX(ts,fidx) ) return true; + } + return false; + } + + private static boolean publicFieldFIDX(TypeStruct clz, int fidx) { + boolean hasCtor = clz.field("<ctor>") != null; + for( Field fld : clz._fields ) + if( fld._fname.charAt(0)!='_' && fld._t instanceof TypeFunPtr tfp && + // With a user constructor, class:N.N is the private <init>. + !(hasCtor && clz.hiddenInit(fld)) && + XInt.bit(tfp._fidxs,fidx) ) + return true; + return false; + } + + public static boolean hasUserConstructor(TypeStruct self) { + Type t = Parser.TYPES.get(Parser.addClzPrefix(self._name)); + return t instanceof TypeStruct clz && clz.field("<ctor>") != null; + } + + // A forward reference gets a class object and constructor signature so + // parsing can proceed, but it has no callable definition yet. + private static boolean resolvedClass(TypeStruct clz) { + Type t = Parser.TYPES.get(clz._name.substring(Parser.CLZ.length())); + return !(t instanceof TypeStruct ts) || !ts._fref; + } + + private int[] _publicFIDXs, _publicAliases; + + /** + * Freeze the externally visible portion of the program after ParseAll has + * loaded every compilation unit and closed/upgraded all types. Later + * optimization can remove linker entries and create inline-only fidxs; + * neither operation changes this interface. + */ + void freezePublicInterface() { + assert _publicFIDXs==null && _publicAliases==null; + int[] fidxs = XInt.EMPTY; + for( int fidx=LocalIds.RESERVED; fidx<_linker._len; fidx++ ) + if( publicFIDX(fidx) ) + fidxs = XInt.make(fidxs,fidx); + int[] aliases = XInt.EMPTY; + for( Type t : Parser.TYPES.values() ) + if( t instanceof TypeStruct clz && + Parser.startsClzPrefix(clz._name) && + Parser.isPublicClassName(clz._name) && + resolvedClass(clz) ) { + boolean hasCtor = clz.field("<ctor>") != null; + for( Field fld : clz._fields ) + if( fld._fname.charAt(0)!='_' && fld._t!=Type.TOP && + !(hasCtor && clz.hiddenInit(fld)) ) + aliases = XInt.make(aliases,fld._alias); + } + _publicFIDXs = fidxs; + _publicAliases = aliases; + } + + public int[] publicFIDXs() { assert _publicFIDXs !=null; return _publicFIDXs; } + public int[] publicAliases(){ assert _publicAliases!=null; return _publicAliases; } + public boolean publicInterfaceFrozen() { return _publicFIDXs != null; } + + // --------------------------- + + // Very common nodes, cached here + public final ConstantNode ZERO; + public final XCtrlNode XCTRL; + + public Node constant( Type t ) { + assert _phase==null || _phase.ordinal() <= Phase.Lower.ordinal(); + return ConstantNode.make(t).peephole(); + } + public ConstantNode con( Type t ) { return (ConstantNode)constant(t); } + public ConstantNode con( long con ) { return con==0 ? ZERO : con(TypeInteger.constant(con)); } + + + // --------------------------- + // Test setup; no module nor file with specific argument + public CodeGen( String src ) { this(src, 126L, true ); } + + // Test setup; no module nor file; can alter seed & argument; can re-run same CodeGen + public CodeGen( String src, Type arg ) { + this(null,null,null, src, 126L, true, arg); + } + // Test setup; no module nor file; can alter seed & argument; can re-run same CodeGen + public CodeGen( String src, long workListSeed, boolean resetTypes ) { + this(null,null,null, src, workListSeed, resetTypes, TypeInteger.BOT); + } + + // Generic CodeGen, including full module setup + public CodeGen( String modDir, String buildDir, + String srcName, String src, long workListSeed, boolean resetTypes, Type arg ) { + // Public singleton to avoid passing about this state to a huge count + // of places. Probably becomes a TLS at some point. + CODE = this; + if( resetTypes ) Type.reset(); // Reset to recover from failed test + + _cwd = System.getProperty("user.dir")+"/"; + _modDir = modDir == null ? _cwd : modDir; + _buildDir = buildDir == null ? _cwd : buildDir; + _srcName = srcName; + _callingConv = null; // Calling convention + // Source code from test strings, not files + _src = src; + // All the compilation units + _compunits = new HashMap<>(); + _phase = null; + // Start GVN table + _gvn = new HashMap<>(); + // Allow whole-suite worklist-order sweeps without rewriting the many + // explicit seeds used by tests and command-line compilation. + String iterSeed = System.getProperty("simple.iter.seed"); + _iter = new IterPeeps(iterSeed == null ? workListSeed : Long.parseLong(iterSeed)); + // End points of graph + _stop = new StopNode().init(); + _start = new StartNode(null,_stop,arg).init(); + ZERO = con(TypeInteger.ZERO).keep(); + XCTRL = new XCtrlNode().peephole().keep(); + P = new Parser(this); + } + + + // Run the requested phases. ELF output contains machine code and data. + + // No code emission, just IR generation + public CodeGen driver( Phase phase ) { return driver(phase,null,null,false,false,0); } + // No object file writing, but code generation for a specific cpu/os pair (allows emulation) + public CodeGen driver( Phase phase, String cpu, String callingConv ) { + if( phase.ordinal() < Phase.Export.ordinal() ) + // No code outputted + return driver(phase,cpu,callingConv,false,false,0); + if( _srcName == null ) + throw new RuntimeException("No source filename provided, so do not know how to name the obj file"); + boolean emitEntrySymbol = !_srcName.contains("."); + return driver( phase, cpu, callingConv, false, emitEntrySymbol, 0 ); + } + // Write an object file for a specific cpu/os pair + + public CodeGen driver( String cpu, String callingConv, boolean inMemory, boolean emitEntrySymbol ) { return driver(Phase.Export,cpu,callingConv,inMemory,emitEntrySymbol,0); } + + // The Generic driver + public CodeGen driver( Phase target, String cpu, String callingConv, boolean inMemory, boolean emitEntrySymbol, int dump ) { + // Parse the entry source and discover all referenced source files. + step(target, Phase.Parse, this::parse, dump); + // Run peepholes to a fixed point + step(target, Phase.Iter, this::iter, dump); + // Run SCCP (and resolve int/flt expressions) + step(target, Phase.Opto, this::opto, dump); + // Run peepholes to a fixed point again, on the better types. + step(target, Phase.IterPostOpto, this::iterPostOpto, dump); + // Field layouts; field symbols become constant offsets + step(target, Phase.FinalizeIR, this::finalizeIR, dump); + // Final error check - bad programs fail here + step(target, Phase.TypeCheck, this::typeCheck, dump); + // Replace "fat" nodes with their exploded explicit IR; this can + // introduce CFG diamonds + step(target, Phase.Lower, this::lower, dump); + // Compute the loop-tree, used for approximating exec frequencies for + // scheduling and register allocation + step(target, Phase.LoopTree, this::loopTree, dump); + // Instruction selection + step(target, Phase.Select, () -> instSelect(cpu,callingConv), dump); + // Global Code Motion + step(target, Phase.Schedule, this::GCM, dump); + // Local (basic block only) scheduling + step(target, Phase.LocalSched, this::localSched, dump); + // Register allocation + step(target, Phase.RegAlloc, this::regAlloc, dump); + // Encode machine instructions into bits + step(target, Phase.Encoding, this::encode, dump); + // Export machine code and data into one ELF file + step(target, Phase.Export, () -> exportELF(inMemory,emitEntrySymbol), dump); + return this; + } + + // The driver names the order; optional phases decide locally whether to run. + private void step(Phase target, Phase next, Runnable work, int dump) { + if( next.ordinal() > target.ordinal() || (_phase!=null && _phase.ordinal()>=next.ordinal()) ) return; + Phase prior = _phase; + work.run(); + if( _phase!=prior ) dump(dump); + } + + // All phase entry points share ordering checks, timing, and viewer notification. + // Record elapsed time even on failure, but report completion only on success. + CodeGen phase(Phase next, boolean ordered, Runnable work) { + assert ordered : "Cannot run "+next+" after "+_phase; + _phase = next; + long t0 = System.currentTimeMillis(); + work.run(); // Do The Phase + _times[next.ordinal()] = System.currentTimeMillis() - t0; + if( _obs!=null ) _obs.phase(next.name()); // Record for viewer + return this; + } + + public String entryClinitName() { + return Parser.addClzPrefix(_srcName==null ? "Test" : _srcName.replace('/','.'))+".<clinit>"; + } + + // Verbose printing during compilation + private void dump(int dump) { + int p2 = _phase.ordinal(); + if( (dump & (1<<p2)) == 0 ) return; + if( (dump & (1<<30)) != 0 ) + System.err.println("After "+_phase+":"); + System.err.println(_phase.ordinal() >= Phase.LocalSched.ordinal() + ? asm(new SB()).toString() + : IRPrinter.prettyPrint(this)); + } + + + // Record times by phase + public final long[] _times = new long[Phase.LastPhase.ordinal()]; + + // --------------------------- + /** + * A counter, for unique node id generation. Starting with value 1, to + * avoid bugs confusing node ID 0 with uninitialized values. + */ + public int _uid = 1; + public int UID() { return _uid; } + public int getUID() { + return _uid++; + } + + + // Allocate dense alias/function/call IDs within this invocation. + // Class slots let a forward reference reserve an initializer's identity. + public final LocalIds _aliases = new LocalIds(); + public int alias(String clz) { return _aliases.next(clz); } + + // C storage has one global alias per symbol, including across object files. + public int externAlias(String name) { return _aliases.next("C:"+name,LocalIds.RESERVED); } + + // External data addresses supplied by an in-memory linker (image offsets). + public final HashMap<String,Integer> _externDataAddresses = new HashMap<>(); + + // Compute local function index (FIDX) from global function info. This is + // called *in order* during parsing, and that order is part of the global + // unique mapping + public final LocalIds _fidxs = new LocalIds(); + // Return local index for specific global file & order. Used to find fidxs for e.g. FREF class <init> fcns + public int fidx( String clz, int order ) { return _fidxs.next(clz, order); } + // New fidx in the current file + public int fidx( String clz ) { return fidx(clz, -1); } + + // Compute local RPC index from global RPC info, one per call + public final LocalIds _rpcs = new LocalIds(); + public int rpc( String clz ) { return _rpcs.next(clz); } + + + // Inlining can insert CFG edges and invalidate cached depth ordering. + private char _iDepthVersion; + public char iDepthVersion() { return _iDepthVersion; } + public void invalidateIDepthCaches() { + assert _iDepthVersion < Character.MAX_VALUE : "Dominator cache version exceeds 65535"; + ++_iDepthVersion; + } + + // 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; + + // Reverse from a constant function pointer to the IR function being called. + // Error to call with a non-constant TFP + public FunNode link( TypeFunPtr tfp ) { return link(tfp.fidx()); } + // Read-only linker lookup, suitable for printers and debugger display. + // In particular, do not lazily remove a partially dead function. + public FunNode _link(int fidx) { return _linker.atX(fidx); } + public FunNode _link(TypeFunPtr tfp) { return _link(tfp.fidx()); } + // Return the FunNode from a fidx + public FunNode link( int fidx ) { + FunNode fun =_linker.atX(fidx); + if( fun!=null && fun.isDead() ) { + _linker.setX(fidx,null); fun = null; } + return fun; + } + // Return the <clinit> for a CompUnit + public FunNode link( CompUnit cu ) { + for( FunNode fun : _linker ) + if( fun!=null && fun._compunit == cu && fun.isClz() ) + return fun; + return null; + } + + // Insert linker mapping from constant function signature to the function + // being called. + public void link(FunNode fun) { + int fidx = fun.sig().fidx(); + _linker.setX(fidx,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). + public final Ary<FunNode> _linker = new Ary<>(FunNode.class); + public final Ary<CallNode> _calls = new Ary<>(CallNode.class); + + // Extern function declarations; input is the FIDX assigned to the name. + final HashMap<Integer,String> _externFunc = new HashMap<>(); + public void externFunc(int fidx, String ex) { + assert !_externFunc.containsKey(fidx); + _externFunc.put(fidx,ex); + } + public String externFunc(int fidx) { return _externFunc.get(fidx); } + + public String funcName(int fidx ) { + FunNode fun = _link(fidx); + if( fun!=null ) return fun._name; + return externFunc(fidx); + } + + public boolean owns(FunNode fun) { + return fun._compunit != null && fun._compunit._src != null; + } + + + // --------------------------- + // Parser object + public final Parser P; + + // Parse ASCII text into Sea-of-Nodes IR + public CodeGen parse() { return phase(Phase.Parse, _phase==null, () -> ParseAll.parse(this,_src)); } + + + // --------------------------- + // 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++; } + + // Pessimistic peepholes after parsing. This lifts loaded and parsed + // types before the optimistic Opto pass resets types and lets them fall. + public CodeGen iter() { return phase(Phase.Iter, _phase==Phase.Parse, () -> Iter.run(this)); } + + // SCCP and numeric-mode resolution; leave cleanup work queued for Iter. + public CodeGen opto() { return phase(Phase.Opto, _phase==Phase.Iter, () -> Opto.opto(this)); } + + public CodeGen iterPostOpto() { return phase(Phase.IterPostOpto, _phase==Phase.Opto, () -> _iter.iterate(this)); } + + public CodeGen finalizeIR() { return phase(Phase.FinalizeIR, _phase==Phase.IterPostOpto, () -> FinalizeIR.run(this)); } + + // Root a surviving function without claiming that it has unknown callers. + void keepInCompUnit(FunNode fun) { + StopCUNode stop = fun._compunit._stop; + if( stop._inputs.find(fun.ret()) == -1 ) + add(stop).addDef(fun.ret()); + } + public <N extends Node> N add( N n ) { return _iter.add(n); } + public void addAll( Ary<Node> ary ) { _iter.addAll(ary); } + public void addAll( Node n ) { _iter.add(n); _iter.addAll(n._inputs); } + + // --------------------------- + // Last check for bad programs + public CodeGen typeCheck() { return phase(Phase.TypeCheck, _phase==Phase.FinalizeIR, () -> TypeCheck.run(this)); } + + public static TypeStruct requiredStruct(Node n) { + // CNC: remove? cleanup? + if( n instanceof NewNode alloc ) return alloc._ts; + if( n instanceof ConFldOffNode off ) return off._ts; + if( n instanceof FldOffNode off && off.in(0)._type instanceof TypeMemPtr ptr ) return ptr._obj; + return null; + } + + // --------------------------- + // TypeCheck queued surviving CLInits during its existing error walk. + public CodeGen lower() { + return phase(Phase.Lower, _phase==Phase.TypeCheck, () -> _iter.iterate(this)); + } + + // --------------------------- + // Build the loop tree; break never-exit loops + public CodeGen loopTree() { + return phase(Phase.LoopTree, _phase==Phase.Lower, () -> { _start.buildLoopTree(_linker,_stop); FinishCalls.run(this); }); + } + + // --------------------------- + + + // --------------------------- + // Code generation CPU target + public Machine _mach; + // Chosen calling convention (usually either Win64 or SystemV) + public String _callingConv; + public String _cCallingConv; + // Callee save registers + public RegMask _callerSave; + public RegMask _cCallerSave; + + // All returns have the following inputs: + // 0 - ctrl + // 1 - memory + // 2 - varies with returning GPR,FPR + // 3 - RPC + // 4+- Caller save registers + public RegMask[] _retMasks; + // Return Program Counter + public RegMask _rpcMask; + + // Convert to target hardware nodes + public CodeGen instSelect( String cpu, String callingConv ) { return instSelect(cpu,callingConv,PORTS); } + public CodeGen instSelect( String cpu, String callingConv, String base ) { + return InstSelect.select(this,cpu,callingConv,base); + } + + // --------------------------- + + + // --------------------------- + // Control Flow Graph in Reverse Post Order. + public Ary<CFGNode> _cfg = new Ary<>(CFGNode.class); + + // Global schedule (code motion) nodes + public CodeGen GCM() { + return phase(Phase.Schedule, (_phase==Phase.LoopTree || _phase==Phase.Select), () -> GlobalCodeMotion.buildCFG(this)); + } + + // --------------------------- + // Local (basic block) scheduler phase, a classic list scheduler + public CodeGen localSched() { return phase(Phase.LocalSched, _phase==Phase.Schedule, () -> ListScheduler.sched(this)); } + + + // --------------------------- + // Register Allocation + public RegAlloc _regAlloc; + public CodeGen regAlloc() { + return phase(Phase.RegAlloc, _phase==Phase.LocalSched, () -> { + _regAlloc = new RegAlloc(this); + _regAlloc.regAlloc(); + }); + } + + // Human-readable register name + public String reg(Node n) { + if( _phase.ordinal() >= Phase.RegAlloc.ordinal() ) { + String s = _regAlloc.reg(n); + if( s!=null ) return s; + } + return "N"+ n._nid; + } + + + // --------------------------- + // Encoding + public Encoding _encoding; + public CodeGen encode() { return phase(Phase.Encoding, _phase==Phase.RegAlloc, () -> _encoding = new Encoding(this).encode()); } + + // --------------------------- + // Exporting to external formats + ElfWriter _elf; + public CodeGen exportELF( boolean inMemory, boolean emitEntrySymbol ) { + return phase(Phase.Export, _phase==Phase.Encoding, () -> { + if( inMemory ) new LinkMem(this).link(_encoding); + else _elf = new ElfWriter(this).export(emitEntrySymbol); + }); + } + + + // --------------------------- + + public boolean _asmLittle=true; + public String asm() { return asm(new SB()).toString(); } + SB asm(SB sb) { + ASMPrinter.print(sb,this); + return sb; + } + + + // Testing shortcuts + public Node ctrl() { return compunit()._stop.ret().ctrl(); } + public Node expr() { return compunit()._stop.ret().expr(); } + public String print() { return compunit()._stop.print(); } + + // Debugging helper + @Override public String toString() { + if( _stop == null ) + return "No StopNode"; + return IRPrinter.prettyPrint(this); + } + + // Debugging helper + public Node f(int idx) { + for( CompUnit cu : _compunits.values() ) { + Node n = (cu._stop == null ? _stop : cu._stop).find(idx); + if( n != null ) return n; + } + return null; + } + + + String printCFG() { + if( _cfg==null ) return "no CFG"; + SB sb = new SB(); + for( CFGNode cfg : _cfg ) { + sb.fix(8,""+(int)cfg._idepth); + IRPrinter.printLine( cfg, sb ); + } + return sb.toString(); + } + + public static void print_as_hex(Encoding enc) { + for (byte b : enc._bits.toByteArray()) { + System.out.print(String.format("%02X", b)); + } + System.out.println(); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java new file mode 100644 index 00000000..c8b0a82c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java @@ -0,0 +1,98 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.util.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import static com.seaofnodes.simple.codegen.CodeGen.CODE; + +// A source file and its class. Parent links follow the source directory tree. +public class CompUnit { + public final CompUnit _par; // Parent, e.g. A/B + final String _fname; // Full slashed file name starting from module root, e.g. A/B/C + final public String _cname; // Full dotted class name starting from module root, e.g. A.B.C + final String _name; // Base name, e.g. C + final File _smp; // Simple source file, e.g. module_root/A/B/C.smp + final public String _src; // Source code, from file or test case + + public String sourceFile() { + return _smp!=null ? _smp.getAbsolutePath() : null; + } + + public TypeStruct _clz; // The one TypeStruct being published + + // _deps records units required for types, code, or class references, + // without requiring their initializers to run. These source + // dependencies may form a cycle. + Ary<CompUnit> _deps; + + // Per-Compilation-Unit Start/StopNodes, keeping alive all exported Nodes. + // Null means no code loaded (yet). + public StartCUNode _start; + public StopCUNode _stop ; + + + // Discovered source file. Source is loaded if required to parse, and null + // otherwise. + CompUnit( CompUnit par, String fname, String cname, String name ) throws IOException { + _par = par; + _fname= fname; + _cname= cname; + _name = name; + _smp = source(fname); + + _src = _smp.exists() ? Files.readString(_smp.toPath()) : null; + } + + // The bundled library is ordinary source. A local sys tree can replace it; + // this chapter never loads Simple code from an object file. + static File source(String fname) { + File file=new File(CODE._modDir,fname+".smp"); + if( file.exists() || !(fname.equals("sys") || fname.startsWith("sys/")) ) return file; + return new File("src/main/smp",fname+".smp"); + } + + // Test case. No source file. Source code passed in as a String. Fake + // class name. + CompUnit( String fname, String src ) { + _par = null; + _name = fname; + _fname= fname; + _cname= fname; + _smp = null; + _src = src; + } + + // Record source discovery without adding an initialization edge. + void addDep(CompUnit dep) { + if( _deps == null ) _deps = new Ary<>(CompUnit.class); + _deps.add(dep); + } + + public void addFun(CodeGen code, FunNode fun) { + assert fun.in(1)==_start; + _stop.addDef(fun.ret()); + code.add(_stop); + _stop.addDep(fun); + } + + void setStart(CodeGen code) { + _stop = new StopCUNode().init(); + _start = new StartCUNode(code._start,_stop, Type.BOTTOM, _fname).init(); + code._stop.addDef(_stop); + } + + @Override public String toString() { + String x = "{"+_cname; + if( _smp!=null && _smp.exists()) x += ",hasSMP"; + if( _src!=null ) x += ",hasSrc"; + x += "}"; + return x; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java new file mode 100644 index 00000000..c52209dd --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java @@ -0,0 +1,460 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.BAOS; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; + +public class ElfWriter { + + final CodeGen _code; + Ary<Section> _sections; + StringSection _strtab; + + ElfWriter( CodeGen code ) { + _code = code; + } + + // ------------------------------------------------------------------------ + abstract public class Section { + static final int SHT_NULL = 0; + static final int SHT_PROGBITS= 1; + static final int SHT_SYMTAB = 2; + static final int SHT_STRTAB = 3; + static final int SHT_RELA = 4; + + final String _name; + final int _type, _entsize, _index, _name_pos, _flags; + int _link, _info; + int _file_offset; + + Section(String name, int type, int entsize, int flags) { + _name = name; + _type = type; + _entsize = entsize; + _flags = flags; + _index = _sections._len; + _sections.push(this); + _name_pos = name.isEmpty() + ? 0 // Null section + : _strtab == null + ? 1 // StringSection + // All the other sections + : _strtab.writeCString(name); + } + + void writeHeader(ByteBuffer out) { + out.putInt(_name_pos); // name + out.putInt(_type); // type + out.putLong(_flags); // flags + out.putLong(0); // addr + out.putLong(_file_offset); // offset + out.putLong(size()); // size + out.putInt(_link); // link + out.putInt(_info); // info + out.putLong(16); // addralign + out.putLong(_entsize); // Fixed-size element size + } + + abstract void write(ByteBuffer out); + abstract int size(); + } + + // ------------------------------------------------------------------------ + // Generic data sections; carries about a BAOS for bits + public class DataSection extends Section { + static final int SHF_WRITE = 1; + static final int SHF_ALLOC = 2; + static final int SHF_EXECINSTR = 4; + static final int SHF_INFO_LINK = 64; + + final BAOS _contents; + + DataSection( String name, int type, int flags, int entsize ) { this(name, type, flags, entsize, new BAOS() ); } + DataSection( String name, int type, BAOS contents, int flags ) { this(name, type, flags, 0, contents ); } + private DataSection( String name, int type, int flags, int entsize, BAOS contents ) { + super(name, type, entsize, flags); + _contents = contents; + } + + @Override void write(ByteBuffer out) { out.put(_contents.toByteArray()); } + @Override int size() { return _contents.size(); } + public void write2( int op ) { + _contents.write(op ); + _contents.write(op>> 8); + } + public void write4( int op ) { + _contents.write(op ); + _contents.write(op>> 8); + _contents.write(op>>16); + _contents.write(op>>24); + } + public void write8( long i64 ) { + write4( (int) i64 ); + write4( (int)(i64>>32)); + } + + } + + // ------------------------------------------------------------------------ + // Section to collect and de-dup strings + public class StringSection extends DataSection { + final HashMap<String,Integer> _dedup = new HashMap<>(); + + StringSection() { + super(".strtab",SHT_STRTAB, 0, 0 ); + int emptyoff = writeCString(""); // Always empty string at offset 0 + assert emptyoff == 0; + int namepos = writeCString(".strtab"); + assert namepos == _name_pos; + } + + public int writeCString(String name) { + if( name==null ) return 0; // Anonymous functions have no name + Integer ii = _dedup.get(name); + if( ii != null ) return ii; + // place name in the string table + int pos = _contents.size(); + _dedup.put(name,pos); + byte[] name_bytes = name.getBytes(); + _contents.write(name_bytes); + // the strings are null terminated + _contents.write(0); + return pos; + } + } + + // ------------------------------------------------------------------------ + // The relocations; 3 words per relocation + public class ReloSection extends DataSection { + ReloSection( int info ) { + this(".rela.text",info); + } + ReloSection(String name,int info) { + super(name, SHT_RELA, SHF_INFO_LINK, 24/*entsize*/); + _link = 2; + _info = info; + } + + void writeReloOffPlus(int offset, int symidx, byte relo) { + writeRelo(offset,symidx,relo,-4); + } + void writeRelo(int offset,int symidx,byte relo,long addend) { + assert symidx > 0; + write8(offset); + write8(((long)symidx << 32L) | relo); + write8(addend); + } + } + + + // ------------------------------------------------------------------------ + // Symbols section; emit the symbols into the data section directly + public class SymbolSection extends DataSection { + public static final int SYMBOL_SIZE = 4+1+1+2+8+8; + final BAOS _globals; + final int _nlocals; + + SymbolSection(int nlocals) { + super(".symtab", SHT_SYMTAB, 0/*flags*/, SYMBOL_SIZE); + _globals = new BAOS(); + _nlocals = nlocals; + } + + // Global symbol index + int gidx() { return _nlocals+(_globals.size()/SYMBOL_SIZE); } + + public int symbol( String name, int shndx, int bind, int type, long value, long size ) { + int gidx = gidx(); // Global index before I write to _globals + BAOS baos = bind==SYM_BIND_LOCAL ? _contents : _globals; + write4(baos,_strtab.writeCString(name)); + int info = (bind << 4) | (type & 0xF); + baos.write(info); + baos.write(0); // other/visibility + write2(baos,shndx); // section index + write8(baos,value); + write8(baos,size); + return bind==SYM_BIND_LOCAL ? -99 : gidx; + } + + public int symbol( String name, int shndx, int bind, int type ) { + return symbol(name,shndx,bind,type,0,0); + } + + // creates function and stores where it starts. + // Filters to just this class.obj file. + void encodeFunctions(StopNode stop, Encoding enc, int text_idx, boolean emitEntrySymbol) { + for( Node stopcu : stop._inputs ) { + for( Node ret : stopcu._inputs ) { + FunNode fun = ((ReturnNode)ret).fun(); + if( !_code.owns(fun) ) + continue; + int end = enc.opStart(fun.ret()) + enc.opLen(fun.ret()); + long value = enc.opStart(fun); + long size = end - value; + if( fun._name != null ) { // Anonymous functions have no name + // The C runtime owns `main` and adapts argc/argv into + // a normal Simple array. Keep the Simple function on + // its native ABI under a private entry symbol. + String name = emitEntrySymbol && "main".equals(fun._name) + ? "simple_main" + : fun._name; + symbol(name, text_idx, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, value, size); + } + } + } + } + + // Insert a default-main for top-level compilations, if the top-level + // does not declare a main. + void symbolEntryClinit(int text_idx, Encoding enc) { + FunNode clinit = null; + String entryName = _code.entryClinitName(); + for( FunNode fun : _code._linker ) + if( fun != null && _code.owns(fun) ) { + if( "main".equals(fun._name) ) + return; // encodeFunctions exported this as simple_main + if( entryName.equals(fun._name) ) + clinit = fun; // Keep top-level entry, if we don't find an explicit main + } + // No top-level main, so the top-level clinit becomes a default main + if( clinit != null ) { + assert enc.opStart(clinit) == 0; + symbol("simple_main",text_idx, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, enc.opStart(clinit), 0); + return; + } + throw new IllegalStateException("Missing entry <clinit> "+entryName); + } + + static void write2( BAOS bits, int op ) { + bits.write(op ); + bits.write(op>> 8); + } + static void write4( BAOS bits, int op ) { + bits.write(op ); + bits.write(op>> 8); + bits.write(op>>16); + bits.write(op>>24); + } + static void write8( BAOS bits, long i64 ) { + write4(bits, (int) i64 ); + write4(bits, (int)(i64>>32)); + } + + @Override + void writeHeader(ByteBuffer out) { + // points to string table section + _link = _strtab._index; + _info = _nlocals; + super.writeHeader(out); + } + + @Override void write(ByteBuffer out) { + out.put(_contents.toByteArray()); + out.put(_globals .toByteArray()); + } + @Override int size() { + return _contents.size() + _globals.size(); + } + } + + public static final int SYM_BIND_LOCAL = 0; + public static final int SYM_BIND_GLOBAL = 1; + public static final int SYM_BIND_WEAK = 2; + + public static final int SYM_TYPE_NOTYPE = 0; + public static final int SYM_TYPE_OBJECT = 1; + public static final int SYM_TYPE_FUNC = 2; + public static final int SYM_TYPE_SECTION = 3; + + public static final int SECTION_HDR_SIZE = 64; + + // ------------------------------------------------------------------------ + + + // ------------------------------------------------------------------------ + + // Native code can live above 4G, outside our four-byte function pointers. + // Give address-taken natives a low entry which tail-jumps through the PLT. + // A weak, stable name makes references from separate objects agree on one + // entry. Do not patch references straight to the local copy: that would + // bypass the linker's choice and break pointer equality across objects. + private int nativeEntry(String name, HashMap<String,Integer> entries, + DataSection text, SymbolSection symbols, ReloSection relos) { + Integer old = entries.get(name); + if( old!=null ) return old; + int offset = text.size(); + text._contents.write(0xE9); // jmp rel32; preserve arguments and return address + text.write4(0); + int target = symbols.symbol(name,0,SYM_BIND_GLOBAL,SYM_TYPE_FUNC); + relos.writeReloOffPlus(offset+1,target,(byte)4/*PLT32*/); + int entry = symbols.symbol("__simple_native$"+name,text._index, + SYM_BIND_WEAK,SYM_TYPE_FUNC,offset,5); + entries.put(name,entry); + return entry; + } + + public ElfWriter export(boolean emitEntrySymbol) { + // Sections are created in the order they are emitted. + _sections = new Ary<>(Section.class); + + // Null section + Section nullSection = new DataSection("",Section.SHT_NULL,0,0); + + // String section next; written out early and available to record/dedup + // strings for all remaining sections + _strtab = new StringSection(); + + // place all the symbols + int nlocals = 1/*null*/+9/*number of sections*/; + SymbolSection symbols = new SymbolSection(nlocals); + + // we've already constructed these entire sections in the encoding phase + Encoding enc = _code._encoding; + // Append ELF-only thunks to a copy. Export must not change instruction + // offsets or the image used by the in-memory linker and emulators. + BAOS textBits = new BAOS(enc._bits.toByteArray(),enc._bits.size()); + DataSection text = new DataSection(".text" , Section.SHT_PROGBITS, textBits, DataSection.SHF_ALLOC | DataSection.SHF_WRITE | DataSection.SHF_EXECINSTR ); + // Constant pool + DataSection rodata = new DataSection(".rodata", Section.SHT_PROGBITS, enc._cpool, DataSection.SHF_ALLOC ); + // Static/global pool + DataSection rwdata = new DataSection(".data" , Section.SHT_PROGBITS, enc._sdata, DataSection.SHF_ALLOC | DataSection.SHF_WRITE ); + + + // populate function symbols + symbols.encodeFunctions(_code._stop, _code._encoding, text._index, emitEntrySymbol); + // Export the source compilation unit <clinit> as the C runtime "main". + // Encoding keeps this function at text offset zero. + if( emitEntrySymbol ) + symbols.symbolEntryClinit(text._index,enc); + + // create external .text relocations + ReloSection relocations = new ReloSection(text._index); + HashMap<String,Integer> nativeEntries = new HashMap<>(); + + for( Node n : enc._externals.keySet()) { + String extern = enc._externals.get(n); + boolean nativeAddress = n instanceof FunPtrNode && + _code.externFunc(((TypeFunPtr)n._type).fidx())!=null; + int symidx = nativeAddress + ? nativeEntry(extern,nativeEntries,text,symbols,relocations) + : symbols.symbol(extern,0,SYM_BIND_GLOBAL,SYM_TYPE_FUNC); + int offset = enc.opStart(n) + enc.opLen(n) - 4; + // Address materialization and static fields name the same thunk; + // direct calls still go straight to the native symbol's PLT entry. + relocations.writeReloOffPlus(offset,symidx,(byte)(nativeAddress ? 2/*PC32*/ : 4/*PLT32*/)); + } + + // Native ELF output is AMD64. Data addresses use PC32, not PLT calls. + for( Node n : enc._externData.keySet() ) { + String extern = enc._externData.get(n); + int symidx = symbols.symbol(extern, 0, SYM_BIND_GLOBAL, SYM_TYPE_OBJECT); + int offset = enc.opStart(n) + enc.opLen(n) - 4; + relocations.writeReloOffPlus(offset, symidx, (byte)2/*PC32*/); + } + + // Stable function identities are also addressable from data, including + // anonymous functions which have no source-level exported name. + HashMap<String,Integer> dataSymbols=new HashMap<>(); + for( FunNode fun : _code._linker ) + if( fun!=null && !fun.isDead() && _code.owns(fun) && enc.opStart(fun)>=0 ) { + String name=_code._fidxs.symbol(fun.sig().fidx()); + dataSymbols.put(name,symbols.symbol(name,text._index,SYM_BIND_GLOBAL,SYM_TYPE_FUNC,enc.opStart(fun),0)); + } + for( StaticData.ObjectData obj : enc._data.objects.values() ) { + int section=obj.external ? 0 : obj.readOnly ? rodata._index : rwdata._index; + dataSymbols.put(obj.symbol,symbols.symbol(obj.symbol,section,SYM_BIND_GLOBAL,SYM_TYPE_OBJECT, + obj.external ? 0 : obj.offset,obj.external ? 0 : obj.size)); + } + for( Encoding.Relo relo : enc._bigCons.values() ) { + StaticData.ObjectData obj=enc._data.object(relo._t); + relocations.writeReloOffPlus(relo._opStart+relo._off,dataSymbols.get(obj.symbol),relo._elf); + } + ReloSection roRelos=new ReloSection(".rela.rodata",rodata._index); + ReloSection rwRelos=new ReloSection(".rela.data",rwdata._index); + for( StaticData.Relocation relo : enc._data.relocations ) { + String name=enc._data.targetSymbol(relo.value()); + String nativeName=relo.value() instanceof TypeFunPtr fp ? _code.externFunc(fp.fidx()) : null; + Integer sym=nativeName==null ? dataSymbols.get(name) + : nativeEntry(nativeName,nativeEntries,text,symbols,relocations); + if( sym==null ) { + sym=symbols.symbol(name,0,SYM_BIND_GLOBAL,SYM_TYPE_NOTYPE); + dataSymbols.put(name,sym); + } + // AMD64 absolute 32/64-bit addresses; linker rejects overflow. + byte kind=(byte)(relo.logSize()==2 ? 10 : 1); + (relo.object().readOnly ? roRelos : rwRelos).writeRelo(relo.object().offset+relo.offset(),sym,kind,0); + } + + // Create a "Simple" section for Simple types and ir + + // Write the local section header symbols + assert nlocals == 1+_sections._len; + symbols.symbol("",0,0,0); // Null symbol at index 0 + for( Section s : _sections ) + symbols.symbol(s._name, s._index, SYM_BIND_LOCAL, SYM_TYPE_SECTION, 0, s.size()); + + int size = 64; // ELF header size + // size of all section data + for( Section s : _sections ) { + s._file_offset = size; + size += s.size(); + } + // headers go at the very end + int shdr_offset = size; + size += SECTION_HDR_SIZE*_sections.len(); + + ByteBuffer out = ByteBuffer.allocate(size); + out.order(ByteOrder.LITTLE_ENDIAN); + + // ELF header + out.put(new byte[]{ 0x7F, 'E', 'L', 'F', 2, 1, 1, 0 }); + out.put(new byte[8]); + out.putShort((short)1 /* ET_REL */); // type + out.putShort((short)62 /* AMD64 */); // machine + out.putInt(1); // version + out.putLong(0); // entry + out.putLong(0); // phoff + out.putLong(shdr_offset); // shoff + out.putInt(0); // flags + out.putShort((short)64); // ehsize + out.putShort((short)0); // phentsize + out.putShort((short)0); // phnum + out.putShort((short)64); // shentsize + out.putShort((short) _sections.len()); // shnum + out.putShort((short) _strtab._index); // shstrndx + + // raw section data + for( Section s : _sections ) + s.write(out); + + // Section headers + for( Section s : _sections ) + s.writeHeader(out); + + assert out.position() == size; + + String objName = _code._buildDir + "/" + _code._srcName + ".o"; + File file = new File(objName); + if( file.getParentFile()!=null ) + file.getParentFile().mkdirs(); + try { + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); + bos.write(out.array()); + bos.close(); + } catch( IOException ioe ) { + // Caller does not want to deal, but it is a failed compilation in any case + throw new RuntimeException(ioe); + } + // Reset for next ELF + _sections = null; + _strtab = null; + return this; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Encoding.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Encoding.java new file mode 100644 index 00000000..06bf14f5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Encoding.java @@ -0,0 +1,570 @@ +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 + * <p> + * 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). + * <p> + * 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<CFGNode> _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<Node,Relo> _bigCons = new HashMap<>(); + public final StaticData _data; + public final HashMap<String,Integer> _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<<log; i++ ) { + bits.write((int)x); + x >>= 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<Node,CFGNode> _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; + // Native functions have no local FunNode, but are still callable. + // Do not treat their addresses as identities of deleted functions. + String nativeName = _code.externFunc(tfp.fidx()); + if( nativeName!=null ) return external(fptr,nativeName); + 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<Node,String> _externals = new HashMap<>(); + public Encoding external( Node n, String extern ) { + _externals.put(n,extern); + return this; + } + + public final HashMap<Node,String> _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<LoopNode,Ary<CFGNode>> rpos = new IdentityHashMap<>(); + _cfg = new Ary<>(CFGNode.class); + rpos.put(_code._start.loop(),_cfg); + BitSet visit = _code.visit(); + + // Do them all except the <clinit>s + Ary<FunNode> 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 <clinit>s last, so when reversed they become first. The + // program entry <clinit> 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<LoopNode,Ary<CFGNode>> 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<CFGNode> 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<CFGNode> 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<CFGNode> 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<CFGNode> 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<len; i++ ) + oldStarts[i] = opStart(_cfg.at(i)); + + // TODO: Rewrite this algo to use the small "_jmps" list of just the + // jumps instead of walking all blocks. + // TODO2: Allow short encodings on non-jumps. This algo doesn't + // really care jumps vs other rip-relative things. + + // Check the size of short jumps, adjust the block starts for a whole + // pass over all blocks. If some jumps go long, they might stretch the + // distance for other jumps, so another pass is needed. + int slide = -1; + while( slide != 0) { // While no fails + slide = 0; + for( int i=0; i<len; i++ ) { + CFGNode bb = _cfg.at(i); + opStartAdd(bb, slide); + // Slide down all other (non-CFG) ops in the block + for( Node n : bb._outputs ) + if( n instanceof MachNode && !(n instanceof CFGNode) && + // Match writeEncodings(): FunPtrs are encoded in the + // block of their control, not again at the Return. + !(n instanceof FunPtrNode && bb instanceof ReturnNode) ) + opStartAdd(n, slide); + if( bb instanceof RIPRelSize riprel ) { + CFGNode target = (bb instanceof IfNode iff ? iff.cproj(0) : (CFGNode)bb.out(0)).uctrlSkipEmpty(); + // Delta is from opStart to opStart. X86 at least counts + // the delta from the opEnd, but we don't have the end until + // we decide the size - so the encSize has to deal + int delta = opStart(target) - opStart(bb); + byte opLen = riprel.encSize(delta); + // Recorded size is smaller than the current size? + if( opLen(bb) < opLen ) { + // Start sliding the code down; record slide amount and new size + slide += opLen - opLen(bb); + opLen(bb, opLen); + } + } + } + } + + + // Go again, inserting padding on function headers. Since no + // short-jumps span function headers, the padding will not make any + // short jumps fail. + for( int i=0; i<len; i++ ) { + CFGNode bb = _cfg.at(i); + // Functions pad to align 16 + if( bb instanceof FunNode ) { + int newStart = opStart(bb)+slide; + slide += (newStart+15 & -16)-newStart; + } + opStartAdd(bb, slide); + for( Node n : bb._outputs ) + if( n instanceof MachNode && !(n instanceof CFGNode) && + !(n instanceof FunPtrNode && bb instanceof ReturnNode) ) + opStartAdd(n, slide); + } + + // Copy/slide the bits to make space for all the longer branches + int grow = opStart(_cfg.at(len-1)) - oldStarts[len-1]; + if( grow > 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); + } + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinalizeIR.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinalizeIR.java new file mode 100644 index 00000000..923486c6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinalizeIR.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import java.util.Arrays; + +// Comnpute field layouts only after SCCP and its peepholes converge. +// Keep surviving functions rooted without introducing unknown callers. +abstract class FinalizeIR { + static void run(CodeGen code) { + // Compute field layouts and replace symbolic field offsets by constants. + for( int i=0; i<code._start.nOuts(); i++ ) { + Node use = code._start.out(i); + if( use instanceof ConFldOffNode off ) { + Node con = off.asOffset(); + assert con != null : off._ts._name+"."+off._fname; + off.subsume(con); + i--; + } + } + code._iter.iterate(code); // Propagate field offsets and clean up. + + // Retain surviving functions for emission without adding unknown callers. + for( FunNode fun : code._linker ) + if( fun != null ) { + // A folded static function field can leave a detached body + // with internal cycles. It has no callers; later phases must + // not revive it merely because its nodes are still allocated. + if( fun.isDead() || fun._type.isHigh() ) { + code._linker.set(fun.sig().fidx(),null); + continue; + } + code.keepInCompUnit(fun); + } + for( CompUnit cu : code._compunits.values() ) { + ProjNode mem = cu._start.proj(1); + if( mem != null ) code.add(mem.unkeep()); + } + // Stable print order: initializers first, then other Returns by NID. + for( Node stop : code._stop._inputs ) + Arrays.sort(stop._inputs._es,0,stop.nIns(), + (x,y) -> ((((ReturnNode)x).fun().isClz() ? -1 : x._nid) - + (((ReturnNode)y).fun().isClz() ? -1 : y._nid) )); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinishCalls.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinishCalls.java new file mode 100644 index 00000000..7ba696d3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/FinishCalls.java @@ -0,0 +1,57 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; + +// Detach analysis call edges after building the loop tree. Every live body +// belongs to this invocation; preserve it for scheduling and emission. +abstract class FinishCalls { + + + static void run(CodeGen code) { + + // The remaining passes assume all calls are unlinked; i.e. we are throwing + // away the Call Graph here. Functions only reachable from internal calls + // need to be re-hooked to stop/start less they go dead. + for( FunNode fun : code._linker ) { + // Already linked to start, not going dead + if( fun==null || fun.isDead() ) + continue; + + // Insert the post-Opto code-generation hook. Unlike an Opto-time + // Start input, this no longer carries unknown-caller semantics. + ReturnNode ret = fun.ret(); + StartNode start = fun._compunit._start; + assert start!=null; + if( fun.nIns() <= 1 || fun.in(1) != start ) { + fun.insertDef(1,start); + for( Node use : fun._outputs ) + if( use instanceof ParmNode parm ) + parm.insertDef(1,ConstantNode.raw(parm._type)); + if( fun._compunit._stop._inputs.find(ret) == -1 ) + fun._compunit._stop.addDef(ret); + } + // Unlink from Call + for( int i=2; i<fun.nIns(); i++ ) + ((CallNode)fun.in(i)).unlink(fun,i--); + if( fun.rpc()==null ) { // Ensure valid RPC + // First make sure a valid RPC; single-call into multi-fun will have each + // function having a single call site, so the RPC becomes a constant and + // folds - but since multiple targets, the Call never inlines. Recreate a + // valid RPC so codegen (and Eval2) understands the calling convention. + ParmNode rpc = new ParmNode( "$rpc", 0, ret.rpc()._type, fun ); + rpc.addDef(ret.rpc()); + ret.setDef( 3, rpc.init() ); + } + } + + code._stop.walk(n -> { + if( n instanceof CallEndNode cend ) + for( int i=1; i<cend.nIns(); i++ ) + if( cend.in(i) instanceof ReturnNode ) + cend.delDef(i--); + if( n instanceof CallNode call ) + call.unlink_all(); + return null; + }); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java new file mode 100644 index 00000000..4477025b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java @@ -0,0 +1,304 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.IterPeeps.WorkList; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +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++ ) + rpo.swap(i,rpo.size()-1-i); + // Set global CFG + code._cfg = rpo; + + schedEarly(code); + + // Break up shared global constants by functions + breakUpGlobalConstants(code._start); + + 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 + assert !( def instanceof ReturnNode && use instanceof CallEndNode ); // All calls unwired now + assert !( def instanceof CallNode && use instanceof FunNode ); // All calls unwired now + visit.set(cfg._nid); + for( Node useuse : cfg._outputs ) + _rpo_cfg(cfg,useuse,visit,rpo); + rpo.add(cfg); + } + + // After early scheduling, every global chain member has Start in slot 0. + // Give each function one private copy of the entire dependency graph. + private static void breakUpGlobalConstants( StartNode start ) { + var globals = new IdentityHashMap<Node,Boolean>(); + var cons = new ArrayList<Node>(); + for( Node con : start._outputs ) + if( con!=null && !(con instanceof CFGNode) && + (con.isConst() || con instanceof MachNode mach && mach.outregmap()!=null) ) { + globals.put(con,true); + cons.add(con.keep()); // Preserve original inputs while rewiring users. + } + + var copies = new IdentityHashMap<FunNode,IdentityHashMap<Node,Node>>(); + for( Node con : cons ) + for( Node use : con._outputs.asAry() ) { + if( use==null || globals.containsKey(use) ) continue; + FunNode fun = useFun(use); + if( fun==null ) continue; // Global metadata has no function owner. + assert CodeGen.CODE._mach==null || CodeGen.CODE.owns(fun); + var local = copies.computeIfAbsent(fun,f -> new IdentityHashMap<>()); + Node copy = cloneGlobal(con,fun,globals,local); + for( int i=0; i<use.nIns(); i++ ) + if( use.in(i)==con ) + use.setDef(i,copy); + } + for( Node con : cons ) con.unkill(); + } + + private static Node cloneGlobal(Node con, FunNode fun, IdentityHashMap<Node,Boolean> globals, + IdentityHashMap<Node,Node> copies) { + Node copy = copies.get(con); + if( copy!=null ) return copy; + copies.put(con,copy=con.copyEmpty()); + copy.addDef(fun); + for( int i=1; i<con.nIns(); i++ ) { + Node def = con.in(i); + copy.addDef(globals.containsKey(def) ? cloneGlobal(def,fun,globals,copies) : def); + } + return copy; + } + + private static FunNode useFun(Node use) { + if( use instanceof ReturnNode ret ) return ret.fun(); + if( use instanceof ParmNode parm ) return parm.fun(); + CFGNode cfg = use.cfg0(); + while( cfg!=null && !(cfg instanceof FunNode) ) + cfg = cfg.idom(); + return (FunNode)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 ); + // In dead infinite loops, entire code blocks may be unreachable + // from below. Reach down from the CFG to their Phis so their + // inputs are scheduled too. + if( cfg instanceof RegionNode ) + for( Node phi : cfg._outputs ) + if( phi instanceof PhiNode ) + _schedEarly(phi,code ); + } + } + + private static void _schedEarly(Node n, CodeGen code) { + if( n==null || code._visit.get(n._nid) ) return; // Been there, done that + assert !(n instanceof CFGNode); + code._visit.set(n._nid); + // Schedule inputs first, except Phis: following their backedges would + // enter a data cycle before its control has been scheduled. + for( Node def : n._inputs ) + if( def!=null && !(def instanceof PhiNode) ) + _schedEarly(def,code); + + // An existing edge 0 already supplies control (or a Phi/Proj binding). + if( n.in(0)==null ) { + // Schedule at deepest input + CFGNode early = code._start; // Maximally early, lowest idepth + 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()]; + // Record Load NIDs at all their CFG block choices, then check against + // Store block choices to force a Load above an anti-dependent Store. + int[] anti = new int[code.UID()]; + Ary<Node> antideps = new Ary<>(Node.class); + // Breadth-first scheduling + breadth(code._stop,ns,late,anti, antideps); + + // 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, int[] anti, Ary<Node> antideps) { + // 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 MemOpNode ld && ld._isLoad ) + for( Node memuse : ld.antiDeps(antideps) ) + if( late[memuse._nid]==null ) + continue outer; + + // All uses done, schedule + _doSchedLate(n,ns,late,anti,antideps); + } + + // A use just finished; reconsider its inputs and waiting loads, + // even when the shared memory input was already scheduled. + for( Node def : n._inputs ) { + if( def==null ) continue; + if( late[def._nid]==null ) work.push(def); + wakeLoads(def,late,work,new BitSet()); + } + if( n instanceof LoopNode loop ) + for( Node phi : loop._outputs ) + if( phi instanceof PhiNode && late[phi._nid]==null ) + work.push(phi); + } + } + + private static void _doSchedLate(Node n, Node[] ns, CFGNode[] late, int[] anti, Ary<Node> antideps) { + // 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).domLCA(lca,null); + + // Loads may need anti-dependencies, raising their LCA + if( n instanceof MemOpNode load && load._isLoad ) + lca = find_anti_dep(lca,load,early,late,anti, antideps); + + // 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 ParmNode parm ) + return late[use._nid]; + 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).domLCA(found,null); // Can be more than one matching input. + + 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 instanceof NeverNode || + lca.idepth() > best.idepth() || + best instanceof IfNode; + } + + // A writer may consume a load's memory through an aggregate. + private static void wakeLoads(Node def, CFGNode[] late, WorkList<Node> work, BitSet visit) { + if( visit.get(def._nid) ) return; + visit.set(def._nid); + for( Node out : def._outputs ) + if( out instanceof MemOpNode ld && ld._isLoad && late[ld._nid]==null ) work.push(ld); + if( def instanceof MemMergeNode ) + for( int i=1; i<def.nIns(); i++ ) + if( def.in(i)!=null ) wakeLoads(def.in(i),late,work,visit); + } + + private static CFGNode find_anti_dep(CFGNode lca, MemOpNode load, CFGNode early, CFGNode[] late, int[] anti, Ary<Node> antideps) { + // We could skip final-field loads here. + // Walk LCA->early, flagging Load's block location choices + for( CFGNode cfg=lca; cfg!=early.idom(); cfg = cfg.idom() ) + anti[cfg._nid] = load._nid; + // Walk load->mem uses, looking for Stores causing an anti-dep + for( Node mem : load.antiDeps(antideps) ) { + if( mem instanceof PhiNode phi ) { + // Inputs carrying different memory might overwrite this alias. + // Raise the LCA for those paths; the unchanged-memory paths + // do not clobber the load. No scheduling edge to the Phi. + 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,anti); + } else { + // Hunt for clobbering stores + assert mem instanceof MemOpNode || mem instanceof EscapeNode || + mem instanceof CallNode || mem instanceof CLInitNode; + assert late[mem._nid]!=null; + lca = anti_dep(load,late[mem._nid],mem.cfg0(),lca,mem,anti); + } + } + return lca; + } + + // + private static CFGNode anti_dep( MemOpNode load, CFGNode stblk, CFGNode defblk, CFGNode lca, Node st, int[] anti ) { + // A conditional writer can precede the load without dominating its late block. + // Walk back to the first overlap with the load's placement range. + // A different Phi input need not be dominated by this memory's + // definition. Stop at its depth rather than looking for that ancestor. + // Retain the sentinel too: a function entry has a null idom. + for( ; stblk != defblk.idom() && stblk.idepth() >= defblk.idepth(); stblk = stblk.idom() ) { + // Store and Load overlap, need anti-dependence + if( anti[stblk._nid]==load._nid ) { + lca = stblk.domLCA(lca,null); // Raise Loads LCA + if( lca == stblk && st != null && !(st instanceof CallNode) && st._inputs.find(load) == -1 ) // And if something moved, + st.addDef(load); // Add anti-dep as well (calls already end their block) + return lca; // Cap this stores' anti-dep to here + } + } + return lca; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/IFG.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/IFG.java new file mode 100644 index 00000000..bcdb14bf --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/IFG.java @@ -0,0 +1,580 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +import java.util.BitSet; +import java.util.IdentityHashMap; + +// Interference Graph +abstract public class IFG { + // Interference Graph Support + + // Map from a Basic Block to Live-Out: {a map from a Live Range to a Def} + private static final IdentityHashMap<CFGNode,IdentityHashMap<LRG,Node>> BBOUTS = new IdentityHashMap<>(); + static void resetBBLiveOut() { + for( IdentityHashMap<LRG,Node> bbout : BBOUTS.values() ) + bbout.clear(); + } + static final IdentityHashMap<LRG,Node> TMP = new IdentityHashMap<>(); + + // Inteference Graph: Array of Bitsets + static final Ary<BitSet> IFG = new Ary<>(BitSet.class); + static void resetIFG() { + for( BitSet bs : IFG ) + if( bs!=null ) bs.clear(); + } + + // Set matching bit + private static void addIFG( LRG lrg0, LRG lrg1 ) { + short x0 = lrg0._lrg, x1 = lrg1._lrg; + // Triangulate + if( x0 < x1 ) _addIFG(x0,x1); + else _addIFG(x1,x0); + } + // Add lrg1 to lrg0's conflict set + private static void _addIFG( short x0, short x1 ) { + BitSet ifg = IFG.atX(x0); + if( ifg==null ) + IFG.setX(x0,ifg = new BitSet()); + ifg.set(x1); + } + + + private static final Ary<CFGNode> WORK = new Ary<>(CFGNode.class); + static void pushWork(CFGNode bb) { + if( WORK.find(bb) == -1 ) + WORK.push(bb); + } + + // ------------------------------------------------------------------------ + // Visit all blocks, using the live-out LRGs per-block and doing a backwards + // walk over each block. At the end of the walk, push the live-out sets to + // prior blocks. Due to loops, no single visitation order suffices. This + // problem can be fairly efficiently solved with a proper LIVE calculation + // and bitsets. In the interests of simplicity (and assumption of smaller + // programs) I am skipping LIVE and directly computing it during the IFG + // building. + + // Start by setting the live-out of the exit block (to empty), and putting + // it on a worklist. + + // - Pull from worklist a block who's live-out has changed + // - Walk backwards, adding interferences and computing live. + // - At block head, "push" the new live-out to prior blocks. + // - - If they pick up new live-outs, put them on the worklist. + + public static boolean build( int round, RegAlloc alloc ) { + // Reset all to empty + resetBBLiveOut(); + resetIFG(); + WORK.clear(); + + // Last block has nothing live out + for( CFGNode bb : alloc._code._cfg ) + if( bb.blockHead()) + WORK.push( bb ); + + // Process blocks until no more changes + while( !WORK.isEmpty() ) + do_block(round,alloc,WORK.pop()); + + if( alloc.success() ) + convert2DAdjacency(alloc); + return alloc.success(); + } + + // Walk one block backwards, compute live-in from live-out, and build IFG + static void do_block( int round, RegAlloc alloc, CFGNode bb ) { + assert bb.blockHead(); + IdentityHashMap<LRG,Node> live_out = BBOUTS.get(bb); + TMP.clear(); + if( live_out != null ) + TMP.putAll(live_out); // Copy bits to temp + + // A backwards walk over instructions in the basic block + for( int inum = bb.nOuts()-1; inum >= 0; inum-- ) { + Node n = bb.out(inum); + if( n.in(0) != bb ) continue; + // In a backwards walk, proj users come before the node itself + if( n instanceof MultiNode ) + for( Node proj : n.outs() ) + if( proj instanceof ProjNode ) + do_node(alloc,proj); + if( n instanceof MachNode ) + do_node(alloc,n); + } + + // The bb head kills register, e.g. a CallEnd and caller-save registers + if( bb instanceof MachNode mach ) + kills(alloc,mach); + + // Push live-sets backwards to priors in CFG. + if( bb instanceof RegionNode ) + for( int i=1; i<bb.nIns(); i++ ) + mergeLiveOut(alloc,bb,i); + else + mergeLiveOut(alloc,bb,0); + } + + private static void do_node(RegAlloc alloc, Node n) { + // Defining means killing live LRG + LRG lrg = alloc.lrg(n); + if( lrg != null ) { + // Check for def-side self-conflict live ranges. These must split, + // and only happens during the first round a particular LRG splits. + selfConflict(alloc,n,lrg); + TMP.remove(lrg); // Kill def + } + + // Phis use and define the same live range, i.e. these LRGs already + // marked conflicted, no need to mark again + if( n instanceof PhiNode ) + return; + + // Kill any killed registers; milli-code routines like New can kill + // will not being a CFG. + if( n instanceof MachNode m ) + kills(alloc,m); + + // Interfere n with all live + if( lrg!=null ) { + // Interfere n with all live + for( LRG tlrg : TMP.keySet() ) { + assert tlrg.leader(); + // Skip self + if( lrg != tlrg && + // And register sets overlap + lrg._mask.overlap(tlrg._mask) ) + // Then tlrg and lrg interfere. If lrg needs the single + // last tlrg register at some point, either tlrg or lrg + // must fail. If *n* (a subset of lrg) needs the single + // last tlrg register then only tlrg must fail. + if( ((MachNode)n).outregmap().size1() ) { + if( !tlrg.clr(lrg._mask.firstReg()) ) // Clear bit, no interference + alloc.fail(tlrg); // Clearing drives mask to empty + } else addIFG(lrg,tlrg); // Add interference + } + } + + + // Check for self-conflict live ranges. These must split, and only + // happens during the first round when a particular LRG splits. + // Also record use-side spills for biased coloring. + // Then make all inputs live. + for( int i=1; i<n.nIns(); i++ ) { + Node def = n.in(i); + if( def==null ) continue; + LRG lrg1 = alloc.lrg(def); + if( lrg1==null ) continue; // Anti-dependence, etc, no register + + // Check use-side for self-conflict; if so we MUST split + selfConflict(alloc,def,lrg1); + + // Look for a must-use single register conflicting with some other must-def. + if( n instanceof MachNode m ) { + RegMask ni_mask = m.regmap(i); + if( ni_mask!=null && ni_mask.size1() ) { // Must-use single register + // Search all current live + for( LRG tlrg : TMP.keySet() ) { + assert tlrg.leader(); + Node live = TMP.get(tlrg); + if( live != def && live instanceof MachNode lmach && lmach.outregmap().overlap(ni_mask) ) { + // Deny the register, since it absolutely must be used here + if( !tlrg.clr(ni_mask.firstReg()) ) + // Then direct reg-reg conflict between use here (at n.in(i)) and def (of tlrg) there. + // Fail the older live range, it must move its register. + alloc.fail( tlrg ); + } + } + } + } + + // All inputs live - except in self conflicts, where we keep the prior def alive + // until it goes. + TMP.put(lrg1,def); + } + } + + // Single-register defines *must* have their register; other live ranges + // *must* avoid this register, so instead of interfering we remove the + // single register from the other rmask. + private static void kills( RegAlloc alloc, MachNode m ) { + RegMask killMask = m.killmap(); + if( killMask==null ) return; + // Kill registers with all live + for( LRG tlrg : TMP.keySet() ) { + assert tlrg.leader(); + // Always, tlrg cannot use kills + if( tlrg._mask.overlap(killMask) ) { + Node live = TMP.get(tlrg); + // Disallow clone-ables from living across killed registers. + // Fail them and let splitting re-clone closer to the target use. + if( live instanceof MachNode mach && mach.isClone() ) + alloc.fail(tlrg); + else if( !tlrg.sub(killMask) ) + alloc.fail(tlrg); + } + } + } + + // Check for self-conflict live ranges. These must split, and only happens + // during the first round a particular LRG splits. + private static void selfConflict(RegAlloc alloc, Node n, LRG lrg) { + selfConflict(alloc,n,lrg,TMP.get(lrg)); + } + private static void selfConflict(RegAlloc alloc, Node n, LRG lrg, Node prior) { + if( prior!=null && prior != n ) { + lrg.selfConflict(prior); + lrg.selfConflict(n); + alloc.fail(lrg); // 2 unrelated values live at once same live range; self-conflict + } + } + + // Merge TMP into bb's live-out set; if changes put bb on WORK + private static void mergeLiveOut( RegAlloc alloc, CFGNode priorbb, int i ) { + CFGNode bb = priorbb.cfg(i); + if( bb == null ) return; // Start has no prior + while( !bb.blockHead() ) bb = bb.cfg0(); + + // Lazy get live-out set for bb + IdentityHashMap<LRG, Node> lrgs = BBOUTS.computeIfAbsent( bb, k -> new IdentityHashMap<>() ); + + for( LRG lrg : TMP.keySet() ) { + Node def = TMP.get(lrg); + // Effective def comes from phi input from prior block + if( def instanceof PhiNode phi && phi.cfg0()==priorbb ) { + assert i!=0; + def = phi.in( i ); + } + // The synthetic exit carries no value on this Phi arm. + if( def._type==Type.TOP ) continue; + Node def_bb = lrgs.get(lrg); + if( def_bb==null ) { + lrgs.put(lrg,def); + pushWork(bb); + } else { + // Alive twice with different definitions; self-conflict + selfConflict(alloc,def,lrg,def_bb); + } + } + } + + + static void convert2DAdjacency( RegAlloc alloc ) { + // Convert the 2-D array of bits (a 1-D array of BitSets) into an + // adjacency matrix. + int maxlrg = alloc._LRGS.length; + for( int i=1; i<maxlrg; i++ ) { + BitSet ifg = IFG.atX(i); + if( ifg != null ) { + LRG lrg0 = alloc._LRGS[i]; + for( int lrg = ifg.nextSetBit(0); lrg>=0; lrg=ifg.nextSetBit(lrg+1) ) { + LRG lrg1 = alloc._LRGS[lrg]; + lrg0.addNeighbor(lrg1); + lrg1.addNeighbor(lrg0); + } + } + } + } + + + // ------------------------------------------------------------------------ + // Color the inference graph. + + // Coloring works by removing "trivial" live ranges from the IFG - those + // live ranges with fewer neighbors than available colors (registers). + // These are trivial because even if every neighbor takes a unique color, + // there's at least one more available to color this live range. + + // If we hit a clique of non-trivial live ranges, we pick one to be "at + // risk" of not-coloring. Good choices include live ranges with a large + // area and few hot uses. + + // Then we reverse and put-back live ranges - picking a color as we go. + // If there's no spare color we'll have to spill this at-risk live range. + + public static boolean color(int round, RegAlloc alloc) { + int maxlrg = alloc._LRGS.length, nlrgs=0; + for( int i=1; i<maxlrg; i++ ) + if( alloc._LRGS[i] != null ) + nlrgs++; + + // Simplify + + // Walk all the LRGS looking for some trivial ones to start with. + // During this pass all LRGS are broken into 3 disjoint sets: + // - trivial, removed from IFG; color_stack[0 to sptr] + // - trivial, not (yet) removed from IFG; color_stack[sptr to swork] + // - unknown; color_stack[work to maxlrg] + + // Gather all not-unified (not-null); separate trivial and non-trivial set. + int sptr = 0, swork = 0; + LRG[] color_stack = new LRG[nlrgs]; + for( int i=0, j=0; i<maxlrg; i++ ) { + LRG lrg = alloc._LRGS[i]; + if( lrg==null ) continue; // Unified lrgs are null here + color_stack[j++] = lrg; + if( lrg.lowDegree() ) swap(color_stack, swork++, j-1); + } + + // Pull all lrgs from IFG, in trivial order if possible + while( sptr < color_stack.length ) { + // Swap best color up front + pickColor(color_stack,sptr,swork,alloc); + + // Pick a trivial lrg, and (temporarily) remove from the IFG. + LRG lrg = color_stack[sptr++]; + // If sptr was swork, then pulled an at-risk lrg + if( sptr > swork ) + swork = sptr; + + // Walk all neighbors and remove + if( lrg._adj != null ) { + for( LRG nlrg : lrg._adj ) { + // Remove and compress out neighbor + if( nlrg.removeCompress(lrg) ) { + // Neighbor is just exactly going trivial as 'lrg' is removed from IFG + // Find "j" position in the color_stack + int jx = swork; while( color_stack[jx] != nlrg ) jx++; + // Add trivial neighbor to trivial list. Pull lrg j out of + // unknown set, since its now in the trivial set + swap(color_stack, swork++, jx); + } + } + } + } + + // Reverse simplify (unstack the color stack), and set colors (registers) for live ranges + while( sptr > 0 ) { + LRG lrg = color_stack[--sptr]; + if( lrg==null ) continue; + RegMaskRW rmask = lrg._mask.copy(); + // Walk neighbors and remove adjacent colors + if( lrg._adj!=null ) { + for( LRG nlrg : lrg._adj ) { + nlrg.reinsert(lrg); + int reg = nlrg._reg; + if( reg!= -1 ) // Failed neighbors do not count + rmask.clr(reg); // Remove neighbor from my choices + } + } + // At-risk live-range did not color? + if( rmask.isEmpty() ) { + alloc.fail(lrg); + lrg._reg = -1; + } else { + // Pick first available register + short reg = rmask.firstReg(); + // Pick a "good" color from the choices. Typically, biased-coloring + // removes some over-spilling. + if( rmask.size() > 1 ) reg = biasColor(alloc,lrg,reg,rmask); + lrg._reg = reg; // Assign the register + } + } + + return alloc.success(); + } + + // Pick LRG from color stack + private static void pickColor(LRG[] color_stack, int sptr, int swork, RegAlloc alloc) { + // Out of trivial colorable, pick an at-risk to pull + if( sptr==swork ) + swap(color_stack,sptr,pickRisky(color_stack,sptr,alloc)); + // When coloring, we'd like to give more choices; so when coloring we'd + // like to see the single-def first (since no choices anyway), then + // non-split related (so more live ranges get colored), then + // split-related last, so they have more colors to bias towards. + + // Working in reverse, pick first split-related with many regs, then + // those with some regs, then single-def. + int bidx=sptr; + LRG best=color_stack[bidx]; + for( int idx = sptr+1; idx < swork; idx++ ) + if( betterLRG(best,color_stack[idx]) ) + best = color_stack[bidx=idx]; + if( bidx != sptr ) + swap(color_stack,sptr,bidx); // Pick best at sptr + } + + // Returns true if 'lrg' is better than 'best' + private static boolean betterLRG( LRG best, LRG lrg ) { + // If single-def varies, keep the not-single-def + if( best.size1() != lrg.size1() ) + return best.size1(); + // If hasSplit varies, keep the hasSplit + if( best.hasSplit() != lrg.hasSplit() ) + return lrg.hasSplit(); + // If both are single register (so both sizes are 1), + // return a multi-use over a multi-def + if( best.size1() ) + return !best._multiUse && lrg._multiUse; + // Keep large register count + return best.size() < lrg.size(); + } + + // Pick a live range that hasn't already spilled, or has a single-def- + // single-use that are not adjacent. + private static int pickRisky( LRG[] color_stack, int sptr, RegAlloc alloc ) { + int best=sptr; + int bestScore = pickRiskyScore(color_stack[best],alloc); + for( int i=sptr+1; i<color_stack.length; i++ ) { + if( bestScore == 1000000 ) return best; // Already max score + int iScore = pickRiskyScore(color_stack[i],alloc); + if( iScore > bestScore ) + { best = i; bestScore = iScore; } + } + return best; + } + + // Pick a live range to pull, that might not color. + // + // Picking a live range with a very large span, with defs and uses outside + // loops means spilling a relative cheap live range and getting that + // register over a large area. + // + // Picking a live range that is very close to coloring might allow it to + // color despite being risky. + private static int pickRiskyScore( LRG lrg, RegAlloc alloc ) { + // Callee-save live ranges cover the whole function and are cheap to + // split. Rank scheduled area recovered against the loop-scaled cost + // of the def- and use-side moves. + if( lrg._machDef instanceof CalleeSaveNode ) + return areaScore(functionArea(lrg._fun,alloc), + splitCost((Node)lrg._machDef,(Node)lrg._machUse)); + if( lrg._splitDef != null && lrg._splitDef. in(1) instanceof CalleeSaveNode && + lrg._splitUse != null && lrg._splitUse.out(0) instanceof ReturnNode ) + return areaScore(functionArea(lrg._fun,alloc), + splitCost(lrg._splitDef,lrg._splitUse)); + + // Pick single-def clonables that are not right next to their single-use. + // Rank by the area recovered by cloning closer to the use. + if( !lrg._multiDef && lrg._machDef!=null && lrg._machUse!=null && lrg._machDef.isClone() ) { + Node def = ((Node)lrg._machDef); + Node use = ((Node)lrg._machUse); + CFGNode cfg = def.cfg0(); + int area = cfg != use.cfg0() + ? functionArea(lrg._fun,alloc) + : cfg._outputs.find(use)-cfg._outputs.find(def)-1; + // A single-use function address can move next to its use without + // adding an instruction. Prefer that to saving a callee-save + // register just to hold the address. Stay in the same block and + // require a gap so the clone makes progress. + if( def instanceof FunPtrNode && !lrg._multiUse && + cfg == use.cfg0() && area > 0 ) + return 1000000; + if( area > 0 ) + return areaScore(area,loopCost(use)); + } + + // Default for ranges without a specific area/cost estimate. + return 1000 + (lrg._multiUse ? -100 : 0); + } + + private static int functionArea(FunNode fun, RegAlloc alloc) { + int area=0; + for( CFGNode bb : alloc._code._cfg ) + if( bb.fun()==fun ) + area += bb.nOuts(); + return area; + } + + private static int areaScore(int area, int cost) { + return 1000 + (area<<10)/cost; + } + + private static int splitCost(Node def, Node use) { + return loopCost(def) + (use==null ? 0 : loopCost(use)); + } + + private static int loopCost(Node n) { + return 1 << (n.cfg0().loopDepth()*3); + } + + private static short biasColor( RegAlloc alloc, LRG lrg, short reg, RegMask mask ) { + if( mask.size1() ) return reg; + // Check chain of splits up the def-chain. Take first allocated + // register, and if it's available in the mask, take it. + Node def = lrg._splitDef, use = lrg._splitUse; + int tidx=0, cnt=0; + + while( def != null || use != null ) { + if( cnt++ > 10 ) break; + + if( def != null ) { + short bias = biasColor( alloc, def, mask ); + if( bias >= 0 ) return bias; // Good bias + if( bias == -2 ) def = null; // Kill this side, no more searching + else if( (tidx=biasable(def)) == 0 ) def = null; + } + + if( use != null ) { + short bias = biasColor( alloc, use, mask ); + if( bias >= 0 ) return bias; // Good bias + if( bias == -2 ) use = null; // Kill this side, no more searching + else if( biasable(use)==0 ) use = null; + } + + if( def != null ) { + short bias = biasColorNeighbors( alloc, def, mask ); + if( bias >= 0 ) return bias; + // Advance def side + def = def.in(tidx); + if( alloc.lrg(def)==null ) def=null; + } + + if( use != null ) { + short bias = biasColorNeighbors( alloc, use, mask ); + if( bias >= 0 ) return bias; + use = use.out(0); + if( biasable(use)==0 ) use=null; + } + + } + return mask.firstReg(); + } + + private static int biasable(Node split) { + if( split instanceof SplitNode ) return 1; // Yes biasable, advance is slot 1 + if( split instanceof PhiNode phi ) return phi.region() instanceof LoopNode ? 2 : 1; // Prefer the backedge in loops + if( !(split instanceof MachNode mach) ) return 0; // Not biasable + return mach.twoAddress(); // Only biasable if 2-addr + } + + // 3-way return: + // - good bias reg, take it & exit + // - this path is cutoff; do not search here anymore + // - advance this side + private static short biasColor( RegAlloc alloc, Node split, RegMask mask ) { + short bias = alloc.lrg(split)._reg; + if( bias != -1 ) { + if( mask.test(bias) ) return bias; // Good bias + else return -2; // Kill this side + } else return -1; // Advance this side + } + + // Check if we can match "split" color, or else trim "mask" to colors + // "split" might get. + private static short biasColorNeighbors( RegAlloc alloc, Node split, RegMask mask ) { + LRG slrg = alloc.lrg(split); + if( slrg._adj == null ) return -1; // No trimming + + // Can I limit my own choices to valid neighbor choices? + for( LRG alrg : slrg._adj ) { + short reg = alrg._reg; + if( reg == -1 && alrg._mask.size1() ) + reg = alrg._mask.firstReg(); + if( reg != -1 ) { + mask.clr(reg); + if( mask.size1() ) + return mask.firstReg(); + } + } + return -1; // No obvious color choice, but mask got trimmed + } + + private static void swap( LRG[] ary, int x, int y ) { + LRG tmp = ary[x]; ary[x] = ary[y]; ary[y] = tmp; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java new file mode 100644 index 00000000..db35d524 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java @@ -0,0 +1,126 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import java.util.BitSet; +import java.util.IdentityHashMap; + +// Replace ideal nodes with machine nodes while preserving CFG/loop metadata. +// Selection also owns target ABI masks and rebuilding the copied graph's uses. +class InstSelect { + private final CodeGen _code; + private InstSelect(CodeGen code) { _code=code; } + + static CodeGen select(CodeGen code, String cpu, String callingConv, String base) { + // Ideal evaluation schedules the ideal graph without a CPU port. + if( cpu==null ) return code; + return code.phase(CodeGen.Phase.Select, code._phase==CodeGen.Phase.LoopTree, + () -> new InstSelect(code).run(cpu,callingConv,base)); + } + + private void run(String cpu, String callingConv, String base) { + _code._callingConv = callingConv; + + // Look for CPU in fixed named place: + // com.seaofnodes.simple.node.cpus."cpu"."cpu.class" + String clzFile = base+"."+cpu+"."+cpu; + try { _code._mach = Class.forName(clzFile).asSubclass(Machine.class).getDeclaredConstructor(CodeGen.class).newInstance(_code); } + catch( Exception e ) { throw new RuntimeException(e); } + + _code._cCallingConv = _code._mach.cCallingConv(callingConv); + + // Build global copies of common register masks. + long callerSave = _code._mach.callerSave(); + long cCallerSave= _code._mach.callerSave(_code._cCallingConv); + long neverSave = _code._mach. neverSave(); + int maxReg = Math.min(64,_code._mach.regs().length); + assert maxReg>=64 || (-1L << maxReg & callerSave)==0; // No stack slots in callerSave + assert maxReg>=64 || (-1L << maxReg & cCallerSave)==0; // No stack slots in cCallerSave + _code._callerSave = new RegMask(callerSave); + _code._cCallerSave= new RegMask(cCallerSave); + + // Build a Return RegMask array. All returns have the following inputs: + // 0 - ctrl + // 1 - memory + // 2 - varies with returning GPR,FPR + // 3 - RPC + // 4+- Caller save registers + _code._retMasks = new RegMask[(maxReg-_code._callerSave.size()-Long.bitCount( neverSave ))+4]; + for( int reg=0, i=4; reg<maxReg; reg++ ) + if( !_code._callerSave.test(reg) && ((1L<<reg)&neverSave)==0 ) + _code._retMasks[i++] = new RegMask(reg); + _code._rpcMask = new RegMask(_code._mach.rpc()); + _code._retMasks[3] = _code._rpcMask; + + // Convert to machine ops + _code._uid = 1; // All new machine nodes reset numbering + var map = new IdentityHashMap<Node,Node>(); + _instSelect( _code._stop, map ); + _code._stop = (StopNode)map.get(_code._stop ); + StartNode start = (StartNode)map.get(_code._start); + _code._start = start==null ? new StartNode(_code._start) : start; + _instOuts(_code._stop,_code.visit()); + _code._visit.clear(); + + // Replace the CompUnit stop (and list of functions) + // with hardware-specific ones + for( CompUnit cu : _code._compunits.values() ) { + cu._start= (StartCUNode)map.get(cu._start); + cu._stop = (StopCUNode )map.get(cu._stop ); + } + } + + 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 MachNode already, 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 MachNode ) { + 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. + if( n instanceof MemOpNode mem && mem.off() instanceof ExternOffsetNode off ) { + // External fields replace base+offset with symbol+0. Keep the ideal + // declaring type on the address for machine load/store width selection. + TypeMemPtr ptr = (TypeMemPtr)mem.ptr()._type; + n._inputs.set(2,new ExternNode(TypeMemPtr.make((byte)2,ptr._obj,true),off._extern)); + n._inputs.set(3,ConstantNode.raw(TypeInteger.ZERO)); + } + map.put(n, x=_code._mach.instSelect(n) ); + // Carry loop-tree and pre-order info across the ideal->mach transition + if( n instanceof CFGNode ncfg && x instanceof CFGNode xcfg ) { + xcfg._ltree = ncfg._ltree; + xcfg._pre = ncfg._pre; + } + // 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) ); + // Post selection action + if( x instanceof MachNode mach ) { + if( n instanceof ReturnNode ret ) + ((ReturnNode)mach)._fun = (FunNode)map.get(ret._fun); + mach.postSelect(_code); // Post selection action + } + + return x; + } + + 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); + // CallNode special: outputs are partially ordered; CallEnd in slot 0 + if( in instanceof CallNode call && n instanceof CallEndNode cend && call.out(0) != cend ) + call._outputs.swap(0,call.nOuts()-1); + _instOuts(in,visit); + } + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Iter.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Iter.java new file mode 100644 index 00000000..1ffd9737 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Iter.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + +// Establish a pessimistic fixed point after parsing. Later Iter runs +// reuse the worklist and types produced by the preceding phase. +abstract class Iter { + static void run(CodeGen code) { + // Close parser-in-progress states. Seed a fresh pessimistic solve; + // exposure sets start at FULL and can only shrink during Iter. + code._start.walk(n -> { + if( !(n instanceof ConstantNode) ) + n._type = iterEscapeReset(n._type); + code.add(n); + return null; + }); + code._iter.iterate(code); + // Expensive assert. + assert !CodeGen.expensiveAssert() || Opto.fixedPointCheck(code); + } + + private static Type iterEscapeReset(Type t) { + if( t instanceof TypeMem mem ) + return mem._one ? mem : mem.makeFrom(XInt.FULL,XInt.FULL); + if( t instanceof TypeTuple tt ) { + TypeTuple rez = tt; + for( int i=0; i<tt._types.length; i++ ) { + Type tx = iterEscapeReset(tt._types[i]); + if( tx != tt._types[i] ) rez = rez.makeFrom(i,tx); + } + return rez; + } + return t; + } + + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LRG.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LRG.java new file mode 100644 index 00000000..fdde8838 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LRG.java @@ -0,0 +1,240 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.IdentityHashMap; + +/** A Live Range + * <p> + * A live range is a set of nodes and edges which must get the same register. + * Live ranges form an interconnected web with almost no limits on their + * shape. Live ranges also gather the set of register constraints from all + * their parts. + * <p> + * Most of the fields in this class are for spill heuristics, but there are a + * few key ones that define what a LRG is. LRGs have a unique dense integer + * `_lrg` number which names the LRG. New `_lrg` numbers come from the + * `RegAlloc._lrg_num` counter. LRGs can be unioned together - + * (<a href="https://en.wikipedia.org/wiki/Disjoint-set_data_structure">this is the Union-Find algorithm</a>) + * - and when this happens the lower numbered `_lrg` wins. Unioning only + * happens during `BuildLRG` and happens because either a `Phi` is forcing all + * its inputs and outputs into the same register, or because of a 2-address + * instruction. LRGs have matching `union` and `find` calls, and a set + * `_leader` field. + */ +public class LRG { + + // Dense live range numbers + final short _lrg; + + // U-F leader; null if leader + LRG _leader; + + // Choosen register + short _reg; + + // Count of single-register defs and uses + short _1regDefCnt, _1regUseCnt; + // Count of all defs, uses. Mostly interested 1 vs many + boolean _multiDef, _multiUse; + + // A sample MachNode def in the live range + public MachNode _machDef, _machUse; + short _uidx; // _machUse input + + // Some splits used in biased coloring + MachConcreteNode _splitDef, _splitUse; + + // All the self-conflicting defs for this live range + IdentityHashMap<Node,String> _selfConflicts; + + // AND of all masks involved; null if none have been applied yet + RegMask _mask; + + // Adjacent Live Range neighbors. Only valid during coloring + Ary<LRG> _adj; + + // Function fully containing the live range; used to find the stack frame + // layout, for spill op offsets. + final public FunNode _fun; + + public int nadj() { return _adj==null ? 0 : _adj._len; } + + public RegMask useMask() { return _machUse==null ? null : _machUse.regmap(_uidx); } + + void addNeighbor(LRG lrg) { + if( _adj==null ) _adj = new Ary<>(LRG.class); + _adj.push(lrg); + } + + // Remove and compress neighbor list; store ex-neighbor past + // end for unwinding Simplify. True if exactly going low-degree + boolean removeCompress( LRG lrg ) { + _adj.swap(_adj.find(lrg),_adj._len-1); + return _adj._len-- == _mask.size(); + } + + void reinsert( LRG lrg ) { + assert _adj._es[_adj._len]==lrg; + _adj._len++; + } + + // More registers than neighbors + boolean lowDegree() { return nadj() < _mask.size(); } + + LRG( short lrg, FunNode fun ) { _lrg = lrg; _reg = -1; _fun = fun; } + + boolean leader() { return _leader == null; } + + // Fast-path Find from the Union-Find algorithm + LRG find() { + if( _leader==null ) return this; // I am the leader + if( _leader._leader==null ) // I point to the leader + return _leader; + return _rollup(); + } + // Slow-path rollup of U-F + private LRG _rollup() { + LRG ldr = _leader._leader; + // Roll-up + while( ldr._leader!=null ) ldr = ldr._leader; + LRG l2 = this; + while( l2 != ldr ) { + LRG l3 = l2._leader; + l2._leader = ldr; + l2 = l3; + } + return ldr; + } + + // Union `this` and `lrg`, keeping the lower numbered _lrg. + // Includes a number of fast-path cutouts. + LRG union( LRG lrg ) { + assert leader(); + if( lrg==null ) return this; + lrg = lrg.find(); + if( lrg==this ) return this; + return _lrg < lrg._lrg ? _union(lrg) : lrg._union(this); + } + // Union `this` and `lrg`, folding together all stats. + private LRG _union( LRG lrg ) { + assert _fun == lrg._fun; + // Set U-F leader + lrg._leader = this; + // Fold together stats + if( _machDef==null ) { + _machDef = lrg._machDef; + } else if( lrg._machDef!=null ) { + if( _machDef != lrg._machDef ) _multiDef=true; + if( _1regDefCnt==0 ) + _machDef = lrg._machDef; + else if( _machDef==lrg._machDef ) + _1regDefCnt--; + } + _1regDefCnt += lrg._1regDefCnt; + _multiDef |= lrg._multiDef; + + if( _machUse==null ) { + _machUse = lrg._machUse; + _uidx = lrg._uidx; + } else if( lrg._machUse!=null ) { + if( _machUse != lrg._machUse ) _multiUse=true; + if( _1regUseCnt==0 ) { + _machUse = lrg._machUse; + _uidx = lrg._uidx; + } + else if( _machUse==lrg._machUse ) + _1regUseCnt--; + } + _1regUseCnt += lrg._1regUseCnt; + _multiUse |= lrg._multiUse; + + // Fold deepest Split + _splitDef = deepSplit(_splitDef,lrg._splitDef); + _splitUse = deepSplit(_splitUse,lrg._splitUse); + + // Fold together masks + RegMask mask = _mask.and(lrg._mask); + if( mask==null ) + mask = _mask.copy().and(lrg._mask); + _mask = mask; + return this; + } + + private static MachConcreteNode deepSplit( MachConcreteNode s0, MachConcreteNode s1 ) { + return s0==null || (s1!=null && s0.cfg0().loopDepth() < s1.cfg0().loopDepth()) ? s1 : s0; + } + + // Record any Mach def for spilling heuristics + LRG machDef( MachNode def, boolean size1 ) { + if( _machDef!=null && _machDef!=def ) + _multiDef = true; + if( _machDef==null || size1 ) + _machDef = def; + if( size1 ) + _1regDefCnt++; + if( def instanceof SplitNode split ) + _splitDef = deepSplit(_splitDef,split); + return this; + } + + // Record any Mach use for spilling heuristics + LRG machUse( MachNode use, short uidx, boolean size1 ) { + if( _machUse!=null && _machUse!=use ) + _multiUse = true; + if( _machUse==null || size1 ) + { _machUse = use; _uidx = uidx; } + if( size1 ) + _1regUseCnt++; + if( use instanceof SplitNode split ) + _splitUse = deepSplit(_splitUse,split); + return this; + } + + boolean hasSplit() { return _splitDef != null || _splitUse != null; } + short size() { return _mask.size(); } + boolean size1() { return _mask.size1(); } + + + void selfConflict( Node def ) { + if( _selfConflicts == null ) _selfConflicts = new IdentityHashMap<>(); + _selfConflicts.put(def,""); + } + + + // Record intersection of all register masks. + // True if still has registers + boolean and( RegMask mask ) { + RegMask mask2 = mask.and(_mask); + if( mask2==null ) + mask2 = _mask.copy().and(mask); + _mask = mask2; + return !_mask.isEmpty(); + } + // Remove this singular register + // True if still has registers + boolean clr( int reg ) { + if( _mask.clr(reg) ) return true; + _mask = _mask.copy(); // Need a mutable copy + return _mask.clr(reg); + } + // Subtract mask (AND complement) + // True if still has registers + boolean sub( RegMask mask ) { + RegMask mask2 = _mask.sub(mask); + if( mask2==null ) + mask2 = _mask.copy().sub(mask); + _mask = mask2; + return !_mask.isEmpty(); + } + + @Override public String toString() { return toString(new SB()).toString(); } + + public SB toString( SB sb ) { + sb.p("V").p(_lrg); + if( _mask!=null ) _mask.toString(sb); + return sb; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java new file mode 100644 index 00000000..252b5bf0 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// In-memory linking +public class LinkMem { + final CodeGen _code; + LinkMem( CodeGen code ) { _code = code; } + + public CodeGen link(Encoding enc) { + // Patch external calls internally + enc.patchGlobalRelocations(); + + // Place constant pool after code, static data after constants + int code = 0; // Code start + int cpool = (enc._bits .size() + code + 15) & -16; // pad to 16 + int sdata = (enc._cpool.size() + cpool + 15) & -16; + + + // Patch local ops, e.g. loading float constants from the constant pool + for( Node op : enc._bigCons.keySet() ) { + Encoding.Relo relo = enc._bigCons.get(op); + int target = enc._data.address(enc._data.object(relo._t),cpool,sdata); + ((RIPRelSize)relo._op).patch(enc, relo._opStart, enc.opLen(relo._op), target - relo._opStart); + } + + enc._data.link(cpool,sdata); + return _code; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java new file mode 100644 index 00000000..b6b3a22c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java @@ -0,0 +1,261 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.IterPeeps.WorkList; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +import java.util.*; + +public abstract class ListScheduler { + + // eXtra per-node stuff for scheduling. + // Nodes are all block-local, and projections fold into their parent. + private static final IdentityHashMap<Node,XSched> XS = new IdentityHashMap<>(); + private static class XSched { + + 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 + + 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 instanceof ProjNode && !(n.in(0) instanceof CFGNode) ? n.in(0) : 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 if( !def.isMem() ) _ruse = true; // Remote register user + } + // Count remote outputs and allowed registers + if( n instanceof MultiNode ) + for( Node use : n._outputs ) + computeSingleRDef(bb,use); + else + computeSingleRDef(bb,n); + + if( n instanceof NewNode || n instanceof CallNode ) + bb.fun()._hasCalls = true; + return this; + } + + private void computeSingleRDef(CFGNode bb, Node n) { + // See if this is 2-input, and that input is single-def + if( n instanceof MachNode mach && mach.twoAddress() != 0 ) { + XSched xs = XS.get(n.in(mach.twoAddress())); + if( xs != null ) + _single = xs._single; + } + // Visit all outputs + RegMaskRW rmask = !_single && n instanceof MachNode mach && mach.outregmap() != null ? 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(); + } + } + + + // List schedule every block + public static void sched( CodeGen code ) { + XS.clear(); + for( CFGNode bb : code._cfg ) + if( bb.blockHead() ) + local(bb,false); + } + + // Classic list scheduler + private static void local( CFGNode bb, boolean trace ) { + 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,trace); // 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.in(0)==bb && !(use instanceof PhiNode) && 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, boolean trace ) { + int pick = sched; + Node p = blk.at(pick); + int score = score(p); + if( trace ) { System.out.printf("%4d N%d ",score,p._nid); System.out.println(p); } + for( int i=sched+1; i<ready; i++ ) { + Node n = blk.at(i); + int nscore = score(n); + if( trace ) { System.out.printf("%4d N%d ",nscore,n._nid); System.out.println(n); } + if( nscore > score ) + { score = nscore; pick = i; p = n; } + } + if( trace ) { System.out.printf("Pick: N%d %s\n\n",p._nid,p); } + 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 (delay) 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 defining a remote value, just generically stall alot. Value is + // used in a later block, can we delay until the end of this block? + if( xn._rdef ) score = 200; // If defining a remote value, just generically stall alot + boolean flags = false; + if( n instanceof MultiNode ) { + for( Node use : n._outputs ) + flags |= singleUseNotReady( use, xn._single ); + } else + flags |= singleUseNotReady( n, xn._single ); + score += -10 * Math.min( CNT[1], 2 ); + score += -100 * Math.min( CNT[2], 2 ); + if( flags ) return 10; + + // 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 && n.in(i).nOuts()==1 ) + CNT[xd._single || ((MachNode)n).regmap(i)!=null && ((MachNode)n).regmap(i).size1() ? 2 : 1]++; + } + score += 10 * Math.min( CNT[1], 2 ); + score += 100 * Math.min( CNT[2], 2 ); + + // Nothing special + 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. Stall by 1 (2 if single) + // per not-ready. + + // If CFG user in this block, it must be last, so definitely stall + // single-def (which stalls flags on x86,arm until the jmp). + private static boolean singleUseNotReady( Node n, boolean single ) { + // If n can rematerialize, assume allocator will split into private uses + // as needed. No impact on local scheduling. + if( n.nOuts()>1 && n instanceof MachNode mach && mach.isClone() ) + return false; + for( Node use : n.outs() ) { + XSched xu = XSched.get(use); + if( xu != null && xu._n instanceof CFGNode ) return true; + if( xu != null && xu._bcnt > 0 ) + CNT[single ? 2 : 1]++; // Since bcnt>0 or CFG, stall until user is more ready + } + return false; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LocalIds.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LocalIds.java new file mode 100644 index 00000000..462248dd --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/LocalIds.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.codegen; + +import java.util.HashMap; +import java.util.Arrays; + +// Dense IDs for this invocation. A class slot can be reserved before its file +// is parsed; every later reference to that slot gets the same ID. +public class LocalIds { + public static final int RESERVED=2; + private int _next=RESERVED; + private final com.seaofnodes.simple.util.Ary<String> _owners=new com.seaofnodes.simple.util.Ary<>(String.class); + private final HashMap<String,int[]> _slots=new HashMap<>(); + + public int next(String owner) { return next(owner,-1); } + public int next(String owner, int slot) { + int[] ids=_slots.get(owner); + if( ids==null ) ids=new int[]{RESERVED,1}; + if( slot==-1 ) slot=ids[0]++; + else if( slot>=ids[0] ) ids[0]=slot+1; + if( slot>=ids.length ) ids=Arrays.copyOf(ids,Math.max(slot+1,ids.length*2)); + if( ids[slot]==0 ) { ids[slot]=_next++; _owners.setX(ids[slot],owner); } + _slots.put(owner,ids); + return ids[slot]; + } + public String owner(int id) { return _owners.atX(id); } + public String symbol(int id) { return owner(id)+".$fun"+id; } + public int nextInline() { return _next++; } + public int next(int old) { + int id=_next++; + _owners.setX(id,owner(old)); + return id; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Machine.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Machine.java new file mode 100644 index 00000000..3eee8fc3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Machine.java @@ -0,0 +1,127 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.type.TypeInteger; + +abstract public class Machine { + // Human readable machine name. Something like "x86-64" or "arm" or "risc5" + public abstract String name(); + + // Default opcode size for printing; 4 bytes for most 32-bit risc chips + public int defaultOpSize() { return 4; } + + // Human-readable name for a register number, e.g. "RAX" or "R0" + public abstract String[] regs(); + + // Create a split op; any register to any register, including stack slots. + // The FunNode holds the stack layout after RegAlloc, which in turn + // dictates the stack slot (the frame offset) for stack splits. + public abstract SplitNode split( LRG lrg, String kind, byte round ); + + // List of caller-save registers, as a 64bit mask + public abstract long callerSave(); + public long callerSave(String conv) { return callerSave(); } + + // List of never-save registers, e.g. RSP or a ZERO register if you have one + public abstract long neverSave(); + + // Call Argument Mask. Passed in the function signature and argument + // number (2-based; 0 is for control and 1 for memory). Also passed in a 0 + // for the function itself, or for *outgoing* calls, the maximum stack slot + // given to the incoming function arguments (stack slots reserved for + // incoming arguments). + public abstract RegMask callArgMask(TypeFunPtr tfp, int arg, int maxArgSlot); + public RegMask callArgMask(TypeFunPtr tfp, int arg, int maxArgSlot, String conv) { return callArgMask(tfp,arg,maxArgSlot); } + + // Return register mask, based on signature (GPR vs FPR) + public abstract RegMask retMask(TypeFunPtr tfp); + + // Return PC register + public abstract int rpc(); + + // Return a MachNode unconditional branch + public abstract CFGNode jump(); + + // Instruction select from ideal nodes + public abstract Node instSelect( Node n ); + + /** Stack Slot Numbering + +RA gives all registers a number, starting at 0. Stack slots continue this +numbering going up from the last register number. Common register numbers are +0-15 for GPRs, 16-31 for FPRs, 32 for flags, and stack slots starting at +register#33 going up. These numbers are machine-specific, YMMV, etc; e.g. RPC +is only in stack slot 0 on X86; other cpus start with the rpc in a register +which may spill into any generic spill slot. + +For ease of reading and printing, stack slots start again at slot#0 - but +during RA they are actually biased by CPU.MAX_REG. + +Stack Layout during Reg Alloc: + + Slot Value Description + ---- ------- ----------- +MAXREG+ R+1 argR maxArgSlot(calleR tfp), can be 0 + ... +MAXREG+ 1 arg0 + ---- -- Caller ------ +MAXREG+ 0 RPC +MAXREG+ R+E+N PAD optional alignment + +MAXREG+ + Spills Caller Frame Space; N spills +MAXREG+ R+E+1 Spills + +MAXREG+ R+E argE maxArgSlot(calleE tfp), can be 0 + ... +MAXREG+ R+2 arg0 + ---- -- Callee ------- + ... + +Slots defined by maxArgSlot do not have to contain the actual passed argument; +they can be "shadow slots". If they exist, they are property of the frame / +FunNode where the TFP comes from. + +For a given calleR, there are generally many calleE's and they all share the +low-range stack slots. + +Example:, Win64, main(int arg) { calloc(1,size); } + + Post-Warp: SP+56 shadow-main-arg +Pre-alloc Post-Alloc SP+48 RPC +--- ------- ---------- ------------- +40 spill#3 SP+40 SP+40 spill#3 +39 spill#2 SP+32 "" +38 spill#1 SP+24 "" +37 spill#0 SP+16 "" spill#0 +36 shadow-calloc#size SP+ 8 SP+ 8 shadow-calloc#size +35 shadow-calloc#1 SP+ 0 SP+ 0 shadow-calloc#1 +--- --- FRAME BASE ---------- ---- FRAME BASE ------- +34 shadow-main-arg SP+56 +33 RPC SP+48 + + + */ + + + // Maximum stack slot (or 0) for the args in this TFP. This will include + // shadow slots if defined in the ABI, even if all arguments are passed in + // registers. + public abstract short maxArgSlot(TypeFunPtr tfp); + public short maxArgSlot(TypeFunPtr tfp, String conv) { return maxArgSlot(tfp); } + + // Calling convention used for external C symbols. + public String cCallingConv(String callingConv) { return callingConv; } + + // Only the declared C result width describes the physical return register. + // Simple functions already return canonical full-width integer values. + protected static TypeInteger cReturn(ProjNode prj) { + if( prj._idx==2 && prj.in(0) instanceof CallEndNode cend && + cend.call().fptr()._type instanceof TypeFunPtr tfp && tfp.isConstant() && + CodeGen.CODE.externFunc(tfp.fidx())!=null && + tfp.ret() instanceof TypeInteger ret && ret.log_size()<3 ) + return ret; + return null; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Opto.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Opto.java new file mode 100644 index 00000000..4db277df --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/Opto.java @@ -0,0 +1,317 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; + +import java.util.BitSet; + +// Optimistic Algorithm +abstract public class Opto { + + // Optimistic analysis - extended SCCP + + // Start all Types at TOP, and propagate changes until stable. Unlike a + // normal Sparse Conditional Constant Propagation, this version is + // interprocedural, computing effectively CFA0 in linear time using the + // normal SCCP algorithm. + + // This algo also builds a Call Graph; as Calls become reachable (CONTROL + // not TOP nor XCONTROL), they "link" against the reaching function + // pointers - adding edges from the Call to the Fun, and from the Return to + // the CallEnd. This allows the normal algorithm to use the normal edge + // propagation rules as function argument types stabilize. In the end, we + // get a fairly precise set of Call Graph edges between CallNodes and + // FunNodes. + public static void opto(CodeGen code) { + // Reset Types before running the worklist algorithm + Ary<Type> oldTypes = new Ary<>(Type.class); // Used for asserts only + resetToTop(code,oldTypes); + unlinkStart(code); + + // OPTIMISTIC INTERPROCEDURAL SCCP + sccp(code, oldTypes); + + // SCCP can leave a recursive numeric component at BOTTOM with no + // arithmetic mode selected. Resolve the whole component from all + // available int/float evidence before IterPeeps consumes the types. + resolveNumericModes(code); + + if( !code._iter._work.isEmpty() ) + sccp(code, oldTypes); + + // After SCCP add everything changed to the worklist and see if + // peepholes can make more progress. + moveChangesToWorklist(code, oldTypes); + } + + // Reset Types before running the worklist algorithm + private static void resetToTop(CodeGen code, Ary<Type> oldTypes ) { + code._start.walk( x -> { + oldTypes.setX(x._nid,x._type); // Record original types for asserts + x._type = Type.TOP; // Reset all Nodes to TOP + code._iter.add(x); // Visit everybody at least once; most Nodes produce better than TOP + return null; + } ); + } + + + // If wholeWorld, remove the Start inputs to functions - they are not + // called unless they get linked (hence go dead and can be removed). If + // not wholeWorld, keep only public methods (and not the std lib). + private static void unlinkStart(CodeGen code) { + // Keep the Start & Start.ProjMem alive even as we remove all uses + for( CompUnit cu : code._compunits.values() ) { + ProjNode mem = cu._start.proj(1); + if( mem!=null ) mem.keep(); + } + + // Iterate the Stop-of-Stops + for( Node stop : code._stop._inputs ) { + for( int i=0; i<stop.nIns(); i++ ) { + ReturnNode ret = (ReturnNode)stop.in(i); + FunNode fun = ret.fun(); + // Start is a semantic unknown-caller edge. Public functions + // keep it; closed-world functions must be reached by a + // concrete Call edge discovered during SCCP. Initializers, + // including private ones, are called by opaque CLInit + // obligations whose Call edges are introduced only in Lower. + if( !fun.isClz() && + !code.publicFIDX(fun.sig().fidx()) && + fun.in(1) instanceof StartNode ) { + fun.removeDeadPath(1); + // Unhook from Stop without treating the ReturnNode as + // DEAD. It stays in limbo until Opto links when a caller + // is discovered (or not, and the function is really dead). + stop._inputs.del(i--); + ret.delUse(stop); + } + // Unlink the existing conservative call linkages. + for( int j=1; j<fun.nIns(); j++ ) + if( fun.in(j) instanceof CallNode call ) + call.unlink(fun,j--); + } + } + } + + // As part of building a CallGraph, when opto finds a function ptr flowing + // into a Call, link the Call and Fun. + private static void linkCG(CodeGen code, TypeFunPtr tfp, CallNode call) { + if( tfp.nargs() != call.nargs() ) return; // Error calls hit this + int[] fidxs = tfp.fidxs(); + // An unresolved forward call can still carry the infinite, inferred + // function set into Opto. There is no finite call graph to link yet; + // type checking will report the unresolved reference afterwards. This + // can yet be valid if the call is eventually dead. + // `if( false ) frefCall(args);` + if( XInt.isHigh(fidxs) ) return; + for( int fidx = XInt.next(fidxs,0); fidx >=0; fidx = XInt.next(fidxs,fidx) ) { + // unlinkStart deliberately leaves uncalled functions in a + // temporarily dead limbo. A newly discovered call revives them; + // do not use the cleanup lookup which removes dead linker entries. + FunNode fun = code._link(fidx); + // null here means an external function; i.e. this Call + // calls to an outside library and all its arguments escape. + if( fun != null && !call.linked(fun) ) { + call.link( fun ); + code._iter.add(fun); + code._iter.addAll(fun._outputs); + } + } + } + + // Start added some FIDXs, the same functions now can be reached from start + // and called by anybody. Add the CG edges. + private static void linkStart( CodeGen code, TypeTuple tt ) { + TypeMem tmem = (TypeMem)tt._types[1]; + int[] fidxs = tmem._escFs; + if( XInt.isHigh(fidxs) ) { + // A scalar BOTTOM can flow into memory while compiling an error + // program. It exposes all public functions, not private fields. + for( FunNode fun : code._linker ) + if( fun != null && code.publicFIDX(fun.sig().fidx()) ) + linkStart(code,fun); + return; + } + for( int fidx = XInt.next(fidxs,0); fidx >=0; fidx = XInt.next(fidxs,fidx) ) { + FunNode fun = code._linker.atX(fidx); + if( fun==null ) assert code._externFunc.containsKey(fidx); + else linkStart(code,fun); + } + } + + // Function is now reachable by unknown callers through Start. The + // default inputs must therefore use conservative declared types. + private static void linkStart( CodeGen code, FunNode fun ) { + assert !fun.isDead(); + StartCUNode start = fun._compunit._start; + if( !fun.unknownCallers() ) { + assert !start.isDead(); + code.keepInCompUnit(fun); + // Function is reachable by any *remote* caller who gets the pointer! + fun.insertDef(1,start); + code._iter.add(fun); + for( Node p : fun._outputs ) + if( p instanceof ParmNode parm ) { + Node defalt = parm._idx==1 ? start.proj(1) : ConstantNode.seed(parm._declaredType).peephole(); + parm.insertDef(1,defalt); + code.add(parm); + } + } + } + + + // The core SCCP algorithm + private static void sccp( CodeGen code, Ary<Type> oldTypes ) { + // Iter workList is empty at this point + // OPTIMISTIC PASS + Node n; + int count = 0; // Debug counter + while( (n = code._iter._work.pop()) != null ) { + if( n.isDead() ) continue; + count++; + Type oval = n._type, nval = n.compute(); + if( oval == nval ) continue; + assert oval.isa(nval) : "Non-monotonic SCCP: "+n.getClass().getSimpleName()+"#"+n._nid+" "+n+" old="+oval+" new="+nval; // Types start high and always fall + Type pesiVal = oldTypes.atX(n._nid); + assert pesiVal==null || nval.isa(pesiVal) : "SCCP below pessimistic type: "+n.getClass().getSimpleName()+"#"+n._nid+" "+n+" pessimistic="+pesiVal+" new="+nval; // Never fall worse than the pessimistic pass + n._type = nval; + + // Now we have a series of stanzas where we lazily create the Call + // Graph - adding graph edges between every call site and called + // function, including external (unknown) callers. These edges are + // not in from the start because they are *too many*; we would get + // O(n^2) edges. So all along we have been treating them as + // "virtual" CFG edges - a FunNode taking a Start input is treated + // as if every call on the planet might call it. Now we make these + // CG edges *concrete*, adding them back as we discover that a call + // calls a particular function. + + // If a TFP adds a new function input to a call, link to the new + // Fun. This adds new edges to the graph allowing argument values + // to flow from calls into functions. The added edges are + // effectively a refined Call Graph. + if( nval instanceof TypeFunPtr tfp ) { + for( Node use : n._outputs ) + if( !use._type.isHigh() && use instanceof CallNode call && call.fptr() == n ) + linkCG(code,tfp,call); + } + + // Link a Call which becomes alive. Like above, adds new Call + // Graph edges to the graph. + if( n instanceof CallNode call && nval == Type.CONTROL && call.fptr()._type instanceof TypeFunPtr ) + linkCG(code,call.tfp(),call); + + // If an otherwise-dead function pointer escapes, any future linked + // caller might find and call it. Force the function to be alive + // and called by Start. + if( n instanceof StartNode ) + linkStart(code,(TypeTuple)n._type); + + // Since n._type changed, visit all output neighbors + code._iter.addAll(n._outputs); + n.moveDepsToWorklist(code._iter); + // Quadratic (expensive) small-step assert. + assert !CodeGen.expensiveAssert(count) || worklistCheck(code); + } + } + + // Quadratic (expensive) fixed-point assert + public static boolean fixedPointCheck(CodeGen code) { + return fixedPointCheck(code,code._stop); + } + public static boolean fixedPointCheck(CodeGen code, Node root) { + root.walk( x -> { + Type xval = x.compute(); + assert xval == x._type; + return null; + }); + return true; + } + + // Quadratic (expensive) small-step assert for SCCP, where changed nodes + // may already be on the worklist. + private static boolean worklistCheck(CodeGen code) { + code._midAssert = true; + Node root = code._start; + root.walk( x -> { + Type xval = x.compute(); + assert xval == x._type || (x._type.isa( xval ) && code._iter._work.on( x )) + : "Non-monotonic or missed SCCP work: "+x.getClass().getSimpleName()+"#"+x._nid+" "+x+" old="+x._type+" new="+xval; + return null; + }); + code._midAssert = false; + return true; + } + + // After SCCP add everything changed to the worklist and see if peepholes + // can make more progress. + private static void moveChangesToWorklist(CodeGen code, Ary<Type> oldTypes) { + code._start.walk( x -> { + assert x.compute() == x._type; // Hit the fixed point + assert x._nid >= oldTypes._len || x._type.isa(oldTypes.at(x._nid)); // Hit at least the bottom-up type + code.add(x); + return null; + }); + } + + // Resolve arithmetic modes which are ambiguous only because they belong to + // a recursive component. Float wins because integer operands can be + // converted to float. + private static void resolveNumericModes(CodeGen code) { + final BitSet visited = new BitSet(); + code._stop.walk(n -> { + if( n instanceof ModeNode modeNode && modeNode.mode()==0 ) { + visited.clear(); + visited.set(n._nid); // Inspect inputs, not Bool's integer result + int mode = 0; + for( int i=1; i<n.nIns(); i++ ) + mode |= numericEvidence(n.in(i),visited); + // A closed recursive numeric component can have no remaining + // concrete evidence after SCCP, e.g. dead all-integer loops. + // With no float evidence, settle to integer. + if( mode == 0 ) + mode = 1; + if( mode != 0 ) { + // If both evidence, FP wins + modeNode.setMode((byte)((mode&2)==2 ? 2 : 1)); + code.add(n); + code.addAll(n.outs()); + } + } + return null; + }); + } + + // Gather numeric evidence through Phi and interprocedural return cycles. + // The visited set makes self- and mutually-recursive calls finite. + private static int numericEvidence(Node n, BitSet visited) { + if( n == null ) return 0; + if( visited.get(n._nid) ) return 0; + visited.set(n._nid); + + Type t = n._type; + if( t instanceof TypeFloat ) return 2; + if( t instanceof TypeInteger ) return 1; + if( t!=Type.BOTTOM && !(t instanceof TypeTuple) ) + return 0; // Something unrelated, like a ptr + + int evidence = 0; + assert !(n instanceof ConvertNode); // No test case + switch( n ) { + case ModeNode modeNode when modeNode.mode() != 0 -> { return modeNode.mode(); } + case ModeNode modeNode -> {} + case PhiNode phi -> {} + case ReturnNode ret -> {} + case ProjNode proj when proj.in(0) instanceof CallEndNode cend -> { + for( int i=1; i<cend.nIns(); i++ ) + evidence |= numericEvidence(cend.in(i),visited); + } + default -> { return 0; } + } + for( int i=1; i<n.nIns(); i++ ) + evidence |= numericEvidence(n.in(i),visited); + + return evidence; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java new file mode 100644 index 00000000..171d9923 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java @@ -0,0 +1,285 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.print.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.io.File; +import java.io.IOException; +import java.util.*; + +// Parse a given file, and all its dependencies +public abstract class ParseAll { + + static void parse(CodeGen code, String src) { + Parser.TYPES.clear(); + Parser.TYPES.putAll(Parser.INIT_TYPES); + if( src != null ) { + // No source file, just the source itself. + parseSource(code,code._srcName==null ? "Test" : code._srcName,src); + } else { + // Path from module root to source file + parsePath(code,code._srcName); + } + } + + // Worklist of symbols to search and files to parse + private static final Ary<CompUnit> WORK = new Ary<>(CompUnit.class); + + + // Return a compilation unit (cached or new). If new, it does not necessarily + // need to be compiled. + public static CompUnit makeCUnit( CodeGen code, CompUnit par, String name ) throws IOException { + int idx=name.indexOf('/'); + if( idx>=0 ) return makeCUnit(code,makeCUnit(code,par,name.substring(0,idx)),name.substring(idx+1)); + String fname=par==null ? name : par._fname+"/"+name; + CompUnit cu=code._compunits.get(fname); + if( cu==null ) { + String cname=par==null ? name : par._cname+"."+name; + code._compunits.put(fname,cu=new CompUnit(par,fname,cname,name)); + } + return cu; + } + + + // Parse a top-level string, used for e.g. testing + static void parseSource( CodeGen code, String src ) { + parseSource(code,"Test",src); + } + + // Parse a top-level string with an explicit compilation-unit name. + static void parseSource( CodeGen code, String fname, String src ) { + CompUnit cunit = new CompUnit(fname,src); + code._compunits.put(cunit._fname,cunit); + parseAll(code,cunit); + } + + // module_root/.../src.smp is source file name. + static void parsePath(CodeGen code, String fname) { + try { + // Split and build the parent-path down to the source file. + CompUnit cu = makeCUnit( code, null, fname ); + // Compile this CompUnit + parseAll(code,cu); + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + + private static void enqueue(CompUnit cu) { + if( cu._par!=null ) enqueue(cu._par); + if( cu._start==null && WORK.find(cu)==-1 ) WORK.add(cu); + } + + private static void parseAll(CodeGen code, CompUnit cunit) { + WORK.clear(); // Cleanup from prior tests + // Prime the pump with the first CompUnit + enqueue(cunit); + // Work through all unparsed compilation units and parse + while( !WORK.isEmpty() ) { + CompUnit cu = WORK.remove(0); + if( cu._src==null ) + throw new RuntimeException("Missing source file '"+cu._fname+".smp'"); + parseOne(code,cu); + } + + // Many types will have been declared cyclically either internally or + // across multiple compilation units. + + // Close over all recursive types, and upgrade TYPES + Ary<TypeStruct> ary = new Ary<>(TypeStruct.class); + for( Type t : Parser.TYPES.values() ) + if( t instanceof TypeStruct ts ) + ary.add(ts); + TypeStruct[] tss = Type.closeOver(ary.asAry(),Parser.TYPES); + for( TypeStruct ts : tss ) + Parser.TYPES.put(ts._name,ts); + for( CompUnit cu : code._compunits.values() ) + cu._clz = (TypeStruct)Parser.TYPES.get(Parser.addClzPrefix(cu._cname)); + + // Walk over all Nodes, and upgrade the internal constants to the + // closed-over types. + code._stop.walk( (Node n) -> { + n.upgradeType(Parser.TYPES); + if( n instanceof FRefNode fref ) { + boolean alloc = fref._name.endsWith(".<new>"); + String clzName = alloc + ? fref._name.substring(0,fref._name.length()-".<new>".length()).intern() + : fref._name; + TypeStruct clz = (TypeStruct)Parser.TYPES.get(clzName); + if( clz == null ) + return null; + Node con; + if( alloc ) { + Field ctor = clz.field("<new>"); + if( ctor == null || !(ctor._t instanceof TypeFunPtr tfp) ) { + String name = clzName.startsWith(Parser.CLZ) + ? clzName.substring(Parser.CLZ.length()) : clzName; + int dot = name.lastIndexOf('.'); + throw Parser.error("Unknown struct type '"+(dot==-1 ? name : name.substring(dot+1))+"'",fref._loc); + } + FunNode fun = code.link(tfp); + if( fun == null ) return null; + con = new FunPtrNode(tfp,code._start,fun.ret()).peephole(); + } else { + // Class pointer constant: + TypeMemPtr clzptr = TypeMemPtr.make((byte)2,clz,true); + con = code.con(clzptr); + } + // Replace and optimize + fref.addDef(con); + code.add(fref); + } + // Adds sort by NIDs, which just got shuffled + if( n instanceof AddNode add ) + code.add(add); + return null; + } ); + + // The complete public interface is now known. Keep this independent + // of later graph/linker liveness so SCCP computations remain local and + // monotonic. + code.freezePublicInterface(); + } + + // Parse one Simple source code file. Add all the FRefs produced to the worklist. + private static void parseOne( CodeGen code, CompUnit cunit ) { + cunit.setStart(code); + // Parse a source file, return missing external references + Ary<FRefNode> frefs = code.P.parse(cunit); + + // Find all missing external references, or complain. This can trigger more parses. + while( !frefs.isEmpty() ) { + FRefNode fref = frefs.remove(0); + // Symbol not found in source code (or we would not be here). + // Search the module for the fref + try { + CompUnit xcunit = findCUnitModule(code,cunit,fref._name); + if( xcunit != null ) { + // Replace the FRef with the discovered class name. + fref._name = Parser.addClzPrefix(xcunit._cname); + cunit.addDep(xcunit); + } else { + throw new RuntimeException("Undefined name '" + fref._name +"'"); + } + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + addRequiredTypes(code,cunit); + } + + private static void addRequiredTypes(CodeGen code, CompUnit cunit) { + LinkedHashSet<String> required = new LinkedHashSet<>(); + code._stop.walk(n -> { + TypeStruct ts = CodeGen.requiredStruct(n); + if( ts!=null && ts._fref ) required.add(ts._name); + return null; + }); + for( String typeName : required ) { + try { + CompUnit xcunit = findRequiredType(code,cunit,typeName); + if( xcunit != null ) + cunit.addDep(xcunit); + else + assert false : "expected to find required type"; + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + } + + private static CompUnit findRequiredType(CodeGen code, CompUnit cunit, String typeName) throws IOException { + for( CompUnit cu = cunit; cu != null; cu = cu._par ) { + String symbol = typeName.startsWith(cu._cname+".") + ? typeName.substring(cu._cname.length()+1) + : typeName; + CompUnit xcunit = findCUnitModule(code,cu,symbol); + if( xcunit != null ) + return xcunit; + } + return null; + } + + + // Symbol not found in source code (or we would not be here). + // Search the module for the fref + public static CompUnit findCompUnitOrThrow(CodeGen code, CompUnit cu, String symbol) { + CompUnit cu2 = findCompUnit(code,cu,symbol); + if( cu2 != null ) return cu2; + throw new RuntimeException("Undefined name '" + symbol +"'"); + } + + public static CompUnit findCompUnit(CodeGen code, CompUnit cu, String symbol) { + // Symbol not found in source code (or we would not be here). + // Search the module for the fref + try { + CompUnit local = findCUnitModule(code,cu,symbol); + if( local != null ) + return local; + return null; + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + + // Find the symbol in module, starting from cunit. This searches up the nested + // class chain and sideways 1 step, so that any prefix of the class chain can + // be skipped as a shortcut. Symbol can have dotted names, which then go + // down into classes. e.g. + // A/X/Y exists. + // A/B contains X.Y, which is a shortcut for A.X.Y + + // However if A/B contains "Y", the search would be for A.B.Y then A.Y both of + // which do not exist. + private static CompUnit findCUnitModule( CodeGen code, CompUnit cunit, String symbol ) throws IOException { + if( cunit == null ) { + if( !symbol.equals("sys") || !CompUnit.source("sys").exists() ) return null; + CompUnit sys=makeCUnit(code,null,"sys"); + enqueue(sys); + return sys; + } + + // Check self + if( cunit._name.equals(symbol) ) + return cunit; + + // If symbol is itself nested (has embedded dots), move the leading part + // over to the CompUnit path. So e.g.: + // CompUnit{A.B}, Symbol="X.Y" + // becomes + // CompUnit{A.B.X}, Symbol="Y" + int dotIdx = symbol.indexOf("."); + if( dotIdx != -1 ) { + String pre = symbol.substring(0,dotIdx); + String rest = symbol.substring(dotIdx+1 ); + String fname = cunit._fname + "/" + pre; + File f = CompUnit.source(fname); + if( f.exists() ) { + CompUnit sub = makeCUnit(code,cunit,pre); + // enqueue also parses parents: their initialization must precede a child. + CompUnit dcunit = findCUnitModule(code,sub,rest); + if( dcunit != null ) + return dcunit; // Nested dot search found cunit + } + } + + // Look in mod / fname / name / symbol.smp + String fname = cunit._fname + "/" + symbol; + File f = CompUnit.source(fname); + if( f.exists() ) { + CompUnit sub = makeCUnit(code,cunit,symbol); + // If not seen before or out of date, parse the new nested cunit + if( sub._clz == null && WORK.find(sub) == -1 ) + enqueue(sub); + return sub; + } + + // Look up the scope + return findCUnitModule( code, cunit._par, symbol ); + } + + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RIPRelSize.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RIPRelSize.java new file mode 100644 index 00000000..48319006 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RIPRelSize.java @@ -0,0 +1,14 @@ +package com.seaofnodes.simple.codegen; + +// This Node rip-relative encodings has sizes that vary by delta +public interface RIPRelSize { + // delta is the distance from the opcode *start* to the target start. Each + // hardware target adjusts as needed. X86 measures from the opcode *end* + // and small jumps are 2 bytes, so they'll need to subtract 2. + byte encSize(int delta); + + // Patch full encoding in. Used for both short and long forms. delta + // again is from the opcode *start*. + void patch( Encoding enc, int opStart, int opLen, int delta ); + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java new file mode 100644 index 00000000..ac792988 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java @@ -0,0 +1,780 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import java.util.Arrays; +import java.util.BitSet; +import java.util.IdentityHashMap; + +/** + * "Briggs/Chaitin/Click". + * Graph coloring. + * <p> + * Every def and every use have a bit-set of allowed registers. + * Fully general to bizarre chips. + * Multiple outputs fully supported, e.g. add-with-carry or combined div/rem or "xor rax,rax" setting both rax and flags. + * <p> + * Intel 2-address accumulator-style ops fully supported. + * Op-to-stack-spill supported. + * All addressing modes supported. + * Register pairs could be supported with some grief. + * <p> + * Splitting instead of spilling (simpler implementation). + * Stack slots are "just another register" (tiny stack frames, simpler implementation). + * Stack/unstack during coloring with a marvelous trick (simpler implementation). + * <p> + * Both bitset and adjacency list formats for the interference graph; one of + * the few times it's faster to change data structures mid-flight rather than + * just wrap one of the two. + * <p> + * Liveness computation and interference graph built in the same one pass (one + * fewer passes per round of coloring). + * <p> + * Single-register def or use live ranges deny neighbors their required + * register and thus do not interfere, vs interfering and denying the color + * and coloring time. Basically all function calls do this, but many oddball + * registers also, e.g. older div/mod/mul ops. (5x smaller IFG, the only + * O(n^2) part of this operation). + */ + +public class RegAlloc { + // Main Coloring Algorithm: + // Repeat until colored: + // Build Live Ranges (LRGs) + // - Intersect allowed registers + // If hard conflicts (LRGs with no allowed registers) + // - Pre-split conflicted LRGs, and repeat + // Build Interference Graph (IFG) + // - Self conflicts split now + // Color (remove trivial first then any until empty; reverse assign colors + // - If color fails: + // - - Split uncolorable LRGs + + // Top-level program graph structure + final CodeGen _code; + + public int _spills, _spillScaled; + + private boolean _done; + + // ----------------------- + // Live ranges with self-conflicts or no allowed registers + private final IdentityHashMap<LRG,String> _failed = new IdentityHashMap<>(); + // Loop Phis get one chance to color after only cold self-conflict splits. + // If the same Phi self-conflicts again, split its hot def/backedge also. + private final IdentityHashMap<PhiNode,String> _deferredLoopSelf = new IdentityHashMap<>(); + void fail( LRG lrg ) { + assert lrg.leader(); + _failed.put(lrg,""); + } + boolean success() { return _failed.isEmpty(); } + public boolean done() { return _done; } + + + // ----------------------- + // Map from Nodes to Live Ranges + private final IdentityHashMap<Node,LRG> _lrgs = new IdentityHashMap<>(); + short _lrg_num; + + // Define a new LRG, and assign n + LRG newLRG( Node n, FunNode fun ) { + LRG lrg = lrg(n); + if( lrg!=null ) return lrg; + lrg = new LRG(_lrg_num++, fun); + LRG old = _lrgs.put(n,lrg); assert old==null; + return lrg; + } + + // LRG for n + public LRG lrg( Node n ) { + LRG lrg = _lrgs.get(n); + if( lrg==null ) return null; + LRG lrg2 = lrg.find(); + if( lrg != lrg2 ) + _lrgs.put(n,lrg2); + return lrg2; + } + + // Find LRG for n.in(idx), and also map n to it + LRG lrg2( Node n, int idx ) { + LRG lrg = lrg(n.in(idx)); + return union(lrg,n); + } + + // Union any lrg for n with lrg and map to the union + LRG union( LRG lrg, Node n ) { + LRG lrgn = _lrgs.get(n); + LRG lrg3 = lrg.union(lrgn); + _lrgs.put(n,lrg3); + return lrg3; + } + + // Force all unified to roll up; collect live ranges + LRG[] _LRGS; + void unify() { + Ary<LRG> lrgs = new Ary<>(LRG.class); + for( Node n : _lrgs.keySet() ) { + LRG lrg = lrg(n); + lrgs.setX(lrg._lrg,lrg); + } + _LRGS = lrgs.asAry(); + // Remove unified lrgs from failed set also + for( LRG lrg : _failed.keySet().toArray(new LRG[0]) ) { + if( !lrg.leader() ) { + LRG lrg2 = lrg.find(); + _failed.remove(lrg); + _failed.put(lrg2,""); + } + } + } + + + // Diagnostic lookup: do not compress leaders or rewrite the node mapping. + private LRG _lrg(Node n) { + LRG lrg = _lrgs.get(n); + if( lrg != null ) + while( lrg._leader != null ) lrg = lrg._leader; + return lrg; + } + + public short regnum( Node n ) { + LRG lrg = _lrg(n); + return lrg==null ? -1 : lrg._reg; + } + + // Printable register number for node n + String reg( Node n ) { + LRG lrg = _lrg(n); + return lrg==null ? null : reg(lrg); + } + String reg( LRG lrg ) { + // No register yet, use LRG + if( lrg._reg == -1 ) return "V"+lrg._lrg; + // Chosen machine register unless stack-slot and past RA + String[] regs = _code._mach.regs(); + if( lrg._reg < regs.length || _code._phase.ordinal() <= CodeGen.Phase.RegAlloc.ordinal() || lrg._fun==null ) + return RegMask.reg(regs,lrg._reg); + // Stack-slot past RA uses the frame layout logic + return "[rsp+"+lrg._fun.computeStackOffset(_code,lrg._reg)+"]"; + } + + // ----------------------- + RegAlloc( CodeGen code ) { _code = code; } + + public void regAlloc() { + // Insert callee-save registers + String[] regs = _code._mach.regs(); + long neverSave= _code._mach.neverSave(); + for( CFGNode bb : _code._cfg ) + if( bb instanceof FunNode fun ) { + ReturnNode ret = fun.ret(); + int len = Math.min(regs.length,64); + for( int reg=0; reg<len; reg++ ) + if( !_code._callerSave.test(reg) && ((1L<<reg)&neverSave)==0 ) { + ret.addDef(new CalleeSaveNode(fun,reg,regs[reg])); + assert ret.regmap(ret.nIns()-1).firstReg()==reg; + } + } + // Cache reg masks for New and Call + for( CFGNode bb : _code._cfg ) { + if( bb instanceof CallEndMach cend ) cend.cacheRegs(_code); + for( Node n : bb._outputs ) + if( n instanceof NewNode nnn ) nnn.cacheRegs(_code); + } + // Expensive assert: useful when chasing cross-function allocator edges. + //assert verifyFunctionLocalEdges(); + + // Top driver: repeated rounds of coloring and splitting. + byte round=0; + while( !graphColor(round) ) { + split(round); + if( round >= 7 ) // Really expect to be done soon + throw new IllegalStateException("Register allocation made no progress after eight rounds"); + round++; + } + postColor(); // Remove no-op spills + _done = true; + } + + boolean verifyFunctionLocalEdges() { + for( CFGNode bb : _code._cfg ) + for( Node use : bb._outputs ) { + FunNode useFun = funOf(use); + for( int i=1; i<use.nIns(); i++ ) { + Node def = use.in(i); + if( def==null || def.isConst() || allowedCrossFunctionEdge(use,i,def) ) + continue; + FunNode defFun = funOf(def); + if( defFun != null && useFun != null && defFun != useFun ) + throw new AssertionError(crossFunctionMessage(def,defFun,use,useFun,i)); + } + } + return true; + } + + private static String crossFunctionMessage(Node def, FunNode defFun, Node use, FunNode useFun, int idx) { + StringBuilder sb = new StringBuilder("Cross-function value edge: "); + sb.append(def.uniqueName()).append("(").append(defFun.label()).append(") -> ") + .append(use.uniqueName()).append("(").append(useFun.label()).append(") input ").append(idx) + .append(" def=").append(def).append(" use=").append(use).append("\n all uses:"); + for( Node out : def.outs() ) { + FunNode outFun = funOf(out); + sb.append("\n ").append(out.uniqueName()).append("(") + .append(outFun==null ? "?" : outFun.label()).append(")"); + } + return sb.toString(); + } + + private static boolean allowedCrossFunctionEdge(Node use, int idx, Node def) { + // Function metadata/control edges are not value live ranges. + if( use instanceof FunNode || use instanceof StopNode || use instanceof StopCUNode ) + return true; + // Return-linked function pointers carry function identity/lifetime, not + // an allocator value flowing from the callee body into this function. + if( use instanceof FunPtrNode && def instanceof ReturnNode ) + return true; + // Parm defaults and call-linked argument edges are the entry protocol; + // BuildLRG also skips Parm input 1. + if( use instanceof ParmNode ) + return true; + // Linked CallEnd -> Return edges should be gone before RegAlloc, but + // diagnose allocator value edges rather than call graph metadata here. + if( use instanceof CallEndNode && def instanceof ReturnNode ) + return true; + return false; + } + + private static FunNode funOf(Node n) { + if( n instanceof ParmNode parm ) return parm.fun(); + if( n instanceof ReturnNode ret ) return ret.fun(); + if( n instanceof FunNode fun ) return fun; + if( n instanceof CFGNode cfg ) return cfg.fun(); + CFGNode cfg = n.cfg0(); + return cfg==null ? null : cfg.fun(); + } + + private boolean graphColor(byte round) { + _failed.clear(); + _lrgs.clear(); + _lrg_num = 1; + _LRGS=null; + + return + // Build Live Ranges + BuildLRG.run(round,this) && // if no hard register conflicts + // Build Interference Graph + IFG.build(round,this) && // If no self conflicts or uncolorable + // Conservative coalesce copies + Coalesce.coalesce(round,this) && + // Color attempt + IFG.color(round,this); // If colorable + } + + // ----------------------- + // Split conflicted live ranges. + void split(byte round) { + + // In C2, all splits are handling in one pass over the program. Here, + // in the name of clarity, we'll handle each failing live range + // independently... which generally requires a full pass over the + // program for each failing live range. i.e., might be a lot of + // passes. + + // Sort, to avoid non-deterministic HashMap ordering + LRG[] splits = _failed.keySet().toArray(new LRG[0]); + Arrays.sort(splits, (x,y) -> x._lrg - y._lrg ); + for( LRG lrg : splits ) + split(round,lrg); + } + + // Split this live range, top level heuristic + boolean split( byte round, LRG lrg ) { + assert lrg.leader(); // Already rolled up + + if( lrg._selfConflicts != null ) + return splitSelfConflict(round,lrg); + + // Register mask when empty; split around defs and uses with limited + // register masks. + if( lrg._mask.isEmpty() ) { + if( lrg._1regDefCnt <= 1 && + lrg._1regUseCnt <= 1 && + (lrg._1regDefCnt + lrg._1regUseCnt) > 0 && + // A fixed-register clone with flexible uses must split at the uses. + !(lrg._1regDefCnt==1 && lrg._machDef.isClone() && lrg._1regUseCnt==0) ) + return splitEmptyMaskSimple(round,lrg); + // Repeated single-reg uses from a single def. Special for archs + // with more fixed regs. + if( !lrg._multiDef && lrg._1regDefCnt <= 1 && lrg._1regUseCnt > 2 ) + if( splitEmptyMaskByUse(round,lrg) ) + return true; + } + if( lrg._mask.isEmpty() && lrg._multiDef && lrg._1regUseCnt > 1 && lrg._1regDefCnt <= 1 ) { + if( splitEmptyMaskSingleUses(round,lrg) ) + return true; + } + + // Generic split-by-loop depth. + return splitByLoop(round,lrg); + } + + // Split live range with an empty mask. Specifically forces splits at + // single-register defs or uses and not elsewhere. + boolean splitEmptyMaskSimple( byte round, LRG lrg ) { + // Live range has a single-def single-register, and/or a single-use + // single-register. Split after the def and before the use. Does not + // require a full pass. + + // Compute def nOuts *before* replacing those uses with a split + int defNouts = ((Node)lrg._machDef).nOuts(); + // Split just after def + if( lrg._1regDefCnt==1 && !lrg._machDef.isClone() ) + // Force must-split, even if a prior split same block because register + // conflicts. Example: + // alloc + // V1/rax - forced by alloc + // alloc + // V2/rax - kills prior RAX + // st4 [V1],len - No good, must split around + insertAfterAndReplace( makeSplit("def/empty1",round,lrg), (Node)lrg._machDef, false/*true*/); + // Split just before use + if( lrg._machUse!=null && (lrg._1regUseCnt==1 || (lrg._1regDefCnt==1 && defNouts==1)) ) + insertBefore((Node)lrg._machUse,lrg._uidx,"use/empty1",round,lrg); + return true; + } + + // Popular single-def value: share a split among uses with compatible masks. + boolean splitEmptyMaskByUse( byte round, LRG lrg ) { + Node def = (Node)lrg._machDef; + // Snapshot distinct users: rewriting an input changes def's output list, + // and a call can use the same value in more than one argument. + Ary<Node> uses = new Ary<>(Node.class); + for( Node use : def._outputs ) + if( uses.find(use)==-1 ) uses.push(use); + + Ary<RegMask> rclass = new Ary<>(RegMask.class); + int ncalls=0; + for( Node use : uses ) + if( use instanceof MachNode mach ) { + // Sharing copies across several calls usually loses to their kills. + if( use instanceof CallNode && ++ncalls > 1 ) return false; + for( int i=1; i<use.nIns(); i++ ) + if( use.in(i)==def && mach.regmap(i)!=null ) + putIntoRegClass(rclass,mach.regmap(i)); + } + if( rclass._len <= 1 ) return false; + + for( RegMask rmask : rclass ) { + Node split = makeSplit(def,"popular",round,lrg,rmask); + split.insertAfter(def); + if( split.nIns()>1 ) split.setDef(1,def); + for( Node use : uses ) + if( use instanceof MachNode mach ) + for( int i=1; i<use.nIns(); i++ ) { + RegMask mask = mach.regmap(i); + if( use.in(i)==def && mask!=null && mask.overlap(rmask) ) { + assert rmask.and(mask)==rmask; + use.setDefOrdered(i,split); + } + } + } + return true; + } + + // Empty-mask live range with several direct single-register uses, often + // through a Phi with multiple defs. Split those uses directly instead of + // doing a loop-depth split, which might keep isolating the wrong edge. + boolean splitEmptyMaskSingleUses( byte round, LRG lrg ) { + findAllLRG(lrg); + if( _ns._len <= 5 ) + return false; + boolean progress = false; + for( Node n : _ns ) { + if( !(n instanceof MachNode mach) ) continue; + for( int i=1; i<n.nIns(); i++ ) { + RegMask use = mach.regmap(i); + if( use != null && use.size1() && lrg(n.in(i))==lrg ) { + insertBefore(n,i,"use/emptyN",round,lrg); + progress = true; + } + } + } + return progress; + } + + // Narrowing preserves all earlier users of a class; new classes are disjoint. + // Each use contains its first overlapping class, so one pass suffices. + private static void putIntoRegClass( Ary<RegMask> rclass, RegMask rmask ) { + for( int i=0; i<rclass._len; i++ ) { + RegMask omask = rclass.at(i); + if( !omask.overlap(rmask) ) continue; + if( omask.and(rmask)!=omask ) + rclass.set(i,new RegMask(omask.copy().and(rmask))); + return; + } + rclass.push(rmask); + } + + + // Self conflicts require Phis (or two-address). + // Insert a split after every def. + boolean splitSelfConflict( byte round, LRG lrg ) { + // Sort conflict set, so we're deterministic + Node[] conflicts = lrg._selfConflicts.keySet().toArray(new Node[0]); + Arrays.sort(conflicts, (x,y) -> x._nid - y._nid ); + + // First encounter with a loop Phi: try only cold splits this round. + // Remember the Phi across rounds so a persistent self-conflict forces + // the hot def/backedge splits on the next encounter. + IdentityHashMap<PhiNode,String> deferred = new IdentityHashMap<>(); + for( Node def : conflicts ) { + if( def instanceof PhiNode phi && phi.region() instanceof LoopNode ) + deferLoopSelf(phi,deferred); + for( Node use : def._outputs ) + if( use instanceof PhiNode phi && phi.region() instanceof LoopNode && + phi._inputs.find(def)==2 ) + deferLoopSelf(phi,deferred); + } + + // For all conflicts + for( Node def : conflicts ) { + assert lrg(def)==lrg; // Might be conflict use-side + // Split before each use that extends the live range; i.e. is a + // Phi or two-address + for( int i=0; i<def._outputs._len; i++ ) { + Node use = def.out(i); + if( (use instanceof PhiNode phi && + !(deferred.containsKey(phi) && phi.region() instanceof LoopNode && phi._inputs.find(def)==2) && + !(phi.region() instanceof LoopNode loop && phi.in(2)==def && def.cfg0().idepth() > loop.idepth() ) ) || + (use instanceof MachNode mach && mach.twoAddress()!=0 && use.in(mach.twoAddress())==def) ) + insertBefore( use, use._inputs.find(def), "use/self/use",round,lrg ); + } + // Split after the Phi and before its entry. Also isolate inputs + // shared with another Phi: otherwise the shared input keeps the + // conflicting live ranges joined, even after splitting the entry. + if( def instanceof PhiNode phi && !(def instanceof ParmNode) ) { + if( !deferred.containsKey(phi) ) { + SplitNode split = makeSplit("def/self",round,lrg); + insertAfterAndReplace(split,def,false); + if( split.nOuts()==0 ) + split.killOrdered(); + } + for( int i=1; i<(deferred.containsKey(phi) ? 2 : phi.nIns()); i++ ) + if( i==1 || sharedPhiInput(phi,i) ) + insertBefore(phi,i,"use/self/phi",round,lrg); + } + // Split before two-address ops which extend the live range + if( def instanceof MachNode mach && mach.twoAddress()!= 0 ) + insertBefore(def,mach.twoAddress(),"use/self/two",round,lrg); + } + return true; + } + + private void deferLoopSelf(PhiNode phi, IdentityHashMap<PhiNode,String> deferred) { + if( !_deferredLoopSelf.containsKey(phi) ) { + _deferredLoopSelf.put(phi,""); + deferred.put(phi,""); + } + } + + + private static boolean sharedPhiInput(PhiNode phi, int i) { + for( Node use : phi.in(i)._outputs ) + if( use instanceof PhiNode && use!=phi ) return true; + return false; + } + + // Generic: split around the outermost loop with non-split def/uses. This + // covers both self-conflicts (once we split deep enough) and register + // pressure issues. + boolean splitByLoop( byte round, LRG lrg ) { + findAllLRG(lrg); + + // Find min loop depth for all non-split defs and uses. + long ld = (-1L<<32) | 9999; + for( Node n : _ns ) { + if( lrg(n)==lrg ) // This is a LRG def + ld = ldepth(ld,n,n.cfg0()); + // PhiNodes check all CFG inputs + if( n instanceof PhiNode phi ) { + for( int i=1; i<n.nIns(); i++ ) + if( phi.in(i)._type!=Type.TOP ) + ld = ldepth(ld, phi.in(i), phi.region().cfg(i)); + } else { + // Others check uses + for( int i=1; i<n.nIns(); i++ ) + if( lrgSame(n.in(i),lrg) ) // This is a LRG use + ld = ldepth(ld,n,n.cfg0()); + } + } + int min = (int)ld; + int max = (int)(ld>>32); + + boolean emptyMulti = lrg._mask.isEmpty() && lrg._multiDef && lrg._1regDefCnt <= 1 && + (lrg._1regUseCnt > 1 || _ns._len > 5); + // Multi-def empty-mask conflicts need the hot/deep side isolated from + // fixed register uses. Ordinary pressure splits keep the old shallow + // boundary behavior. + for( Node n : _ns ) { + if( n instanceof SplitNode split && !split.isNonStackSplit() && min!=max ) continue; // Ordinary splits already reach stack storage. + if( n.isDead() ) continue; // Some Cloneable went dead by other spill changes + // If this is a 2-address commutable op (e.g. AddX86, MulX86) and the rhs has only a single user, + // commute the inputs... which chops the LHS live ranges' upper bound to just the RHS. + if( n instanceof MachNode mach && lrg(n)==lrg && mach.twoAddress()==1 && mach.commutes() && n.in(2).nOuts()==1 ) + n.swap12(); + + if( lrg(n)==lrg && // This is a LRG def + // At loop boundary, or splitting in inner loop + (min==max || (emptyMulti ? n.cfg0().loopDepth() >= max : n.cfg0().loopDepth() <= min)) ) { + // Cloneable constants will be cloned at uses, not after def + if( !(n instanceof MachNode mach && mach.isClone()) && + // Single user is already a split adjacent + !(n.nOuts()==1 && n.out(0) instanceof SplitNode split && sameBlockNoClobber(split) ) ) + // Split after def in min loop nest + insertAfterAndReplace( makeSplit("def/loop",round,lrg), n,true); + } + + // PhiNodes check all CFG inputs + if( n instanceof PhiNode phi && !(n instanceof ParmNode)) { + for( int i=1; i<n.nIns(); i++ ) + // No split in front of a split + if( !(n.in(i) instanceof SplitNode) && + // splitting in inner loop or at loop border + (min==max || (emptyMulti ? phi.region().cfg(i).loopDepth() >= max : phi.region().cfg(i).loopDepth() <= min)) && + // and not around the backedge of a loop (bad place to force a split, hard to remove) + !(phi.region() instanceof LoopNode && i==2 && (phi.in(i) instanceof PhiNode pp && pp.region()==phi.region())) ) + // Split before phi-use in prior block + insertBefore(phi,i, "use/loop/phi",round,lrg); + + } else { + // Others check uses + for( int i=1; i<n.nIns(); i++ ) { + // This is a LRG use + // splitting in inner loop or at loop border + if( lrgSame( n.in( i ), lrg ) && + (min == max || (n.in(i) instanceof MachNode mach && mach.isClone()) || + (emptyMulti ? n.cfg0().loopDepth() >= max : n.cfg0().loopDepth() <= min)) ) + // Split before in this block + insertBefore( n, i, "use/loop/use", round,lrg, false ); + } + } + } + return true; + } + + private boolean lrgSame(Node x, LRG lrg) { + LRG xlrg = lrg(x); + while( xlrg==null && x instanceof SplitNode ) + xlrg=lrg(x = x.in(1)); + return xlrg==lrg; + } + + private static long ldepth( long ld, Node n, CFGNode cfg ) { + // Ordinary splits already reach stack storage; register-only transfers do not. + if( n instanceof SplitNode split && !split.isNonStackSplit() ) return ld; + // Collect min/max loop depth + int min = (int)ld; + int max = (int)(ld>>32); + int d = cfg.loopDepth(); + // if n will lower the min loop and is in the tail end of the loop + // header, splitting "around" the loop will not help. Treat n as being + // in the loop. + if( d < min ) { + if( cfg.uctrl() instanceof LoopNode loop && loop.entry()==cfg ) { + for( int i=cfg.nOuts()-2; i>=0; i-- ) { + Node out = cfg.out(i); + if( n==out ) + { d = loop.loopDepth(); break; } // Treat n as being "in the loop" + if( !((out instanceof MachNode mach && mach.isClone()) || out instanceof SplitNode ) ) + break; // Treat b as "normal", out of loop + } + } + } + + // lower min, raise max, and re-fold + min = Math.min(min,d); + max = Math.max(max,d); + return ((long)max<<32) | min; + } + + // Find all members of a live range, both defs and uses + private final Ary<Node> _ns = new Ary<>(Node.class); + void findAllLRG( LRG lrg ) { + _ns.clear(); + int wd = 0; + if( lrg._machDef != null ) _ns.push((Node)lrg._machDef); + if( lrg._machUse != null ) _ns.push((Node)lrg._machUse); + while( wd < _ns._len ) { + Node n = _ns.at(wd++); + if( lrg(n)!=lrg ) continue; + for( Node def : n._inputs ) + if( lrg(def)==lrg && _ns.find(def)== -1 ) + _ns.push(def); + for( Node use : n._outputs ) + if( _ns.find(use)== -1 ) + _ns.push(use); + } + for( Node n : _ns ) assert !n.isDead(); + } + + void insertBefore(Node n, int i, String kind, byte round, LRG lrg, boolean skip) { + Node def = n.in(i); + // Phi splitting may visit the synthetic never-exit arm. + if( def._type==Type.TOP ) return; + // Effective block for use + CFGNode cfg = n instanceof PhiNode phi ? phi.region().cfg(i) : n.cfg0(); + // Use-side RegMask, if available + RegMask umask = n instanceof MachNode mach ? mach.regmap(i) : null; + // Def is a split ? + if( skip && def instanceof SplitNode ) { + boolean singleReg = umask!=null && umask.size1(); + // Same block, multiple registers, split is only used by n, + // assume this is good enough and do not split again. + if( cfg==def.cfg0() && def.nOuts()==1 && !singleReg ) + return; + } + makeSplit(def,kind,round,lrg,umask).insertBefore(n, i); + // Skip split-of-split same block + if( skip && def instanceof SplitNode && cfg==def.cfg0() ) + n.in(i).setDefOrdered(1,def.in(1)); + } + void insertBefore(Node n, int i, String kind, byte round, LRG lrg) { + insertBefore(n,i,kind,round,lrg,true); + } + + // Replace uses of `def` with `split`, and insert `split` immediately after + // `def` in the basic block. + public void insertAfterAndReplace( Node split, Node def, boolean must ) { + split.insertAfter(def); + if( split.nIns()>1 ) split.setDef(1,def); + for( int j=def.nOuts()-1; j>=0; j-- ) { + Node use = def.out(j); + if( use==split ) continue; // Skip self + // Can we avoid a split of a split? 'this' split is used by + // another split in the same block. + if( !must && use instanceof SplitNode split2 && sameBlockNoClobber(split2) ) + continue; + int idx = use._inputs.find(def); + use.setDefOrdered(idx,split); + if( j < def.nOuts() ) j++; + } + } + + private Node makeSplit( Node def, String kind, byte round, LRG lrg, RegMask umask ) { + // Clone simple constants if possible + Node split = def instanceof MachNode mach && mach.isClone() && (umask==null || mach.outregmap().overlap(umask)) + ? mach.copy() + : _code._mach.split(lrg,kind,round); + _lrgs.put(split,lrg); + return split; + } + private SplitNode makeSplit( String kind, byte round, LRG lrg ) { + SplitNode split = _code._mach.split(lrg,kind,round); + _lrgs.put(split,lrg); + return split; + } + + + // ----------------------- + // POST PASS: Remove empty spills that biased-coloring made + private void postColor() { + int maxReg = -1; + for( CFGNode bb : _code._cfg ) { // For all ops + if( bb instanceof FunNode ) + maxReg = -1; // Reset for new function + // Compute frame size, based on arguments and largest reg seen + if( bb instanceof ReturnNode ret ) + ret.fun().computeFrameAdjust(_code,maxReg); + // Raise frame size by max stack args passed, even if ignored + if( bb instanceof CallEndMach cend ) + maxReg = Math.max(maxReg,cend._xslot); + + for( int j=0; j<bb.nOuts(); j++ ) { + Node n = bb.out(j); + if( lrg(n)!=null ) + maxReg = Math.max(maxReg,lrg(n)._reg+1); + // Raise frame size by max stack args passed to New + if( n instanceof NewNode nnn ) + maxReg = Math.max(maxReg,nnn._xslot); + + if( !(n instanceof SplitNode ) ) continue; + int defreg = lrg(n )._reg; + int usereg = lrg(n.in(1))._reg; + // Attempt to bypass split + if( defreg != usereg && splitBypass(bb,j,n,defreg) ) + usereg = lrg(n.in(1))._reg; + + // Same register, or the next instruction can use the source directly. + if( defreg == usereg || splitForward(bb,j,n,usereg) ) { + n.removeSplit(); + j--; + continue; + } + // Split is kept; count against split score + _spills++; + _spillScaled += (1<<bb.loopDepth()*3); + assert _spillScaled >= 0; + } + } + } + + // A sole, adjacent use can read the source register directly. Nothing can + // clobber it in between; only the operand mask and two-address tie remain. + private boolean splitForward( CFGNode bb, int j, Node split, int reg ) { + if( split.nOuts()!=1 || j+1==bb.nOuts() ) return false; + Node use = split.out(0); + if( use!=bb.out(j+1) || use instanceof PhiNode || use instanceof CFGNode || + !(use instanceof MachNode mach) ) return false; + int i = use._inputs.find(split); + RegMask mask = mach.regmap(i); + return mask!=null && mask.test(reg) && mach.twoAddress()!=i; + } + + private boolean splitBypass( CFGNode bb, int j, Node lo, int defreg ) { + // Attempt to bypass split + Node hi = lo.in(1); + while( true ) { + if( !(hi instanceof SplitNode && lo.cfg0() == hi.cfg0()) ) + return false; + if( lrg(hi.in(1))._reg==defreg ) + break; + hi = hi.in(1); + } + // Walk backward from the later copy, including instructions without results. + for( int idx = j-1; bb.out(idx) != hi; idx--) { + Node n = bb.out(idx); + if( clobbers(n,defreg) ) + return false; // Clobbered + } + lo.setDefOrdered(1,hi.in(1)); + return true; + } + + + private boolean clobbers(Node n, int reg) { + LRG lrg = lrg(n); + return lrg!=null && (lrg._reg==reg || lrg._reg==-1 && lrg._mask.size1() && lrg._mask.firstReg()==reg) || + n instanceof MachNode mach && mach.killmap()!=null && mach.killmap().test(reg); + } + + public boolean sameBlockNoClobber( SplitNode split ) { + Node def = split.in(1); + CFGNode cfg = def.cfg0(); + if( cfg != split.cfg0() ) return false; // Not same block + // Get multinode head + Node def0 = def instanceof ProjNode ? def.in(0) : def; + int defreg = lrg(def)._reg; + if( defreg == -1 ) defreg = lrg(def)._mask.firstReg(); + if( defreg == -1 ) return false; // no allowed registers -> clobbered + for( int idx = cfg._outputs.find(split) -1; idx >= 0; idx-- ) { + Node n = cfg.out(idx); + if( n==def0 ) return true; // No clobbers + if( lrg(n) == lrg(def) ) return false; // Self conflict + if( clobbers(n,defreg) ) + return false; // Clobbered + } + throw Utils.TODO("Should not reach here"); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegMask.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegMask.java new file mode 100644 index 00000000..9cb9ec16 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegMask.java @@ -0,0 +1,121 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +/** RegMask + * A "register mask" - 1 bit set for each allowed register. In addition, + * "stack slot" registers may be allowed, effectively making the set infinite. + * <p> + * For smaller and simpler machines it suffices to make such masks an i64 or + * i128 (64- or 128-bit integers), and this presentation is by far the better + * way to go... if all register allocations can fit in this bit limitation. + * The allocator will need bits for stack-based parameters and for splits + * which cannot get a register. For a 32-register machine like the X86, add 1 + * for flags - gives 33 registers. Using a Java `long` has 64 bits, leaving + * 31 for spills and stack passing. This is adequate for nearly all + * allocations; only the largest allocations will run this out. However, if + * we move to a chip with 64 registers we'll immediately run out, and need at + * least a 128 bit mask. Since you cannot *return* a 128 bit value directly + * in Java, Simple will pick up a `RegMask` class object. +*/ +public class RegMask { + + long _bits0, _bits1; + + private static final RegMask EMPTY = new RegMask(0L); + public static final RegMask FULL = new RegMask(-1L); + + public RegMask(int bit) { + if( bit < 64 ) _bits0 = 1L<<bit; + else _bits1 = 1L<<(bit-64); + } + public RegMask(long bits ) { _bits0 = bits; } + public RegMask(long bits0, long bits1 ) { _bits0 = bits0; _bits1 = bits1; } + public RegMask(RegMask r) { _bits0 = r._bits0; _bits1 = r._bits1; } + + // AND, with copy-on-write if changing + RegMask and( RegMask mask ) { + if( mask==null ) return this; + long bits0 = _bits0 & mask._bits0; + long bits1 = _bits1 & mask._bits1; + if( bits0==_bits0 && bits1==_bits1 ) return this; + if( bits0==mask._bits0 && bits1==mask._bits1 ) return mask; + if( bits0==0 && bits1==0 ) return EMPTY; + // Update-in-place a mutable mask, or make a defensive copy + return null; + } + RegMask sub( RegMask mask ) { + if( mask==null ) return this; + long bits0 = _bits0 & ~mask._bits0; + long bits1 = _bits1 & ~mask._bits1; + if( bits0==_bits0 && bits1==_bits1 ) return this; + if( bits0==mask._bits0 && bits1==mask._bits1 ) return mask; + if( bits0==0 && bits1==0 ) return EMPTY; + // Update-in-place a mutable mask, or make a defensive copy + return null; + } + + // Fails if bit is set, because this is immutable + public boolean clr( int reg ) { + if( reg < 64 ) return ((_bits0 >> reg)&1)==0; + if( reg < 128 ) return ((_bits1 >> reg)&1)==0; + return true; + } + + + public short firstReg() { + if( isEmpty() ) return -1; + return (short)(_bits0 != 0 + ? Long.numberOfTrailingZeros(_bits0) + : Long.numberOfTrailingZeros(_bits1)+64); + } + public short nextReg(short reg) { + if( reg < 63 ) { + long bits0 = _bits0 & (-1L << (reg+1)); + if( bits0!=0 ) return (short)Long.numberOfTrailingZeros(bits0); + } + long bits1 = reg < 64 ? _bits1 : reg < 127 ? _bits1 & (-1L << (reg-63)) : 0; + return bits1==0 ? -1 : (short)(64+Long.numberOfTrailingZeros(bits1)); + } + + boolean isEmpty() { return _bits0==0 && _bits1==0; } + + public boolean test( int reg ) { + return ((reg<64 ? (_bits0 >> reg) : (_bits1 >> (reg-64))) & 1) != 0; + } + + // checks if the 2 masks have at least 1 bit in common + public boolean overlap( RegMask mask ) { return (_bits0 & mask._bits0)!=0 || (_bits1 & mask._bits1)!=0; } + + // Defensive writable copy + public RegMaskRW copy() { return new RegMaskRW( _bits0, _bits1 ); } + + // Has exactly 1 bit set + public boolean size1() { + return !isEmpty() && (((_bits0 & -_bits0)==_bits0 && _bits1==0) || + ((_bits1 & -_bits1)==_bits1 && _bits0==0)); + } + + // Cardinality + public short size() { return (short)(Long.bitCount(_bits0)+Long.bitCount(_bits1)); } + + @Override public String toString() { return toString(new SB()).toString(); } + public SB toString(SB sb) { + Machine mach = CodeGen.CODE._mach; + if( _bits0==0 && _bits1==0 ) return sb.p("[]"); + sb.p("["); + String[] regs = mach.regs(); + for( int i=0; i<64; i++ ) + if( ((_bits0 >> i)&1) != 0 ) + sb.p(reg(regs,i)).p(","); + for( int i=0; i<64; i++ ) + if( ((_bits1 >> i)&1) != 0 ) + sb.p(reg(regs,i+64)).p(","); + return sb.unchar().p("]"); + } + + public static String reg(String[] regs, int i) { + return i<regs.length ? regs[i] : "[stk#"+i+"]"; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegMaskRW.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegMaskRW.java new file mode 100644 index 00000000..1da9a3b1 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/RegMaskRW.java @@ -0,0 +1,36 @@ +package com.seaofnodes.simple.codegen; + +// Mutable regmask(writable) +public class RegMaskRW extends RegMask { + public RegMaskRW(long x, long y) { super(x,y); } + // clears bit at position r. Returns true if the mask is still not empty. + public boolean clr(int r) { + if( r < 64 ) _bits0 &= ~(1L<<(r )); + else _bits1 &= ~(1L<<(r-64)); + return _bits0!=0 || _bits1!=0; + } + // sets bit at position r. + public void set(int r) { + if( r < 64 ) _bits0 |= (1L<<(r )); + else _bits1 |= (1L<<(r-64)); + } + @Override RegMaskRW and( RegMask mask ) { + if( mask==null ) return this; + _bits0 &= mask._bits0; + _bits1 &= mask._bits1; + return this; + } + @Override RegMaskRW sub( RegMask mask ) { + if( mask==null ) return this; + _bits0 &= ~mask._bits0; + _bits1 &= ~mask._bits1; + return this; + } + public RegMaskRW or( RegMask mask ) { + if( mask!=null ) { + _bits0 |= mask._bits0; + _bits1 |= mask._bits1; + } + return this; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/SourceError.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/SourceError.java new file mode 100644 index 00000000..f2f46146 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/SourceError.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.util.SB; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +/** Format one parser byte offset. Source is read and text built only on error. */ +public abstract class SourceError { + public static String format(CompUnit cu, int pos, String message) { + String file=cu.sourceFile(); + String name=file==null ? cu._fname+".smp" : file; + byte[] source; + try { + source=cu._src!=null ? cu._src.getBytes(StandardCharsets.UTF_8) + : file==null ? null : Files.readAllBytes(Path.of(file)); + } catch( IOException ex ) { source=null; } + if( source==null || pos<0 || pos>source.length ) + return name+": error: "+message+" (source unavailable at byte offset "+pos+")"; + + int line=1, start=0; + for( int i=0; i<pos; i++ ) + if( source[i]=='\n' ) { line++; start=i+1; } + int end=pos; + while( end<source.length && source[end]!='\n' ) end++; + if( end>start && source[end-1]=='\r' ) end--; + int point=Math.min(pos,end); + String before=new String(source,start,point-start,StandardCharsets.UTF_8); + String text=new String(source,start,end-start,StandardCharsets.UTF_8); + int column=before.codePointCount(0,before.length())+1; + SB sb=new SB().p(name).p(':').p(line).p(':').p(column).p(": error: ").p(message).p("\n "); + int width=0, caret=0; + for( int i=0; i<text.length(); ) { + if( i==before.length() ) caret=width; + int ch=text.codePointAt(i); + if( ch=='\t' ) { + int spaces=8-(width%8); + for( int j=0; j<spaces; j++ ) sb.p(' '); + width+=spaces; + } else { sb.p(new String(Character.toChars(ch))); width++; } + i+=Character.charCount(ch); + } + if( before.length()==text.length() ) caret=width; + sb.p("\n "); + for( int i=0; i<caret; i++ ) sb.p('-'); + return sb.p('^').toString(); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/StaticData.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/StaticData.java new file mode 100644 index 00000000..616753d4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/StaticData.java @@ -0,0 +1,177 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.BAOS; +import java.util.*; + +// Pool objects have identity independently of their initial field values. +// References in their contents use the same symbols in ELF and emulator images. +public final class StaticData { + public static final class ObjectData { + public final Type type; + public final String symbol; + public final boolean readOnly, external; + public final int alignment, size; + public int offset; + ObjectData(Type t, String name, boolean ro, boolean ext) { + type=t; symbol=name; readOnly=ro; external=ext; + alignment=t.alignment(); + int align=1<<alignment; + size=t instanceof TypeStruct ts ? (Math.max(1,ts.size())+align-1)&-align : align; + } + } + public record Relocation(ObjectData object, int offset, int logSize, Type value) {} + + final Encoding enc; + final LinkedHashMap<Object,ObjectData> objects = new LinkedHashMap<>(); + final ArrayList<Relocation> relocations = new ArrayList<>(); + final String prefix; + + public ArrayList<ObjectData> entries() { + ArrayList<ObjectData> entries=new ArrayList<>(objects.values()); + entries.sort(Comparator.comparingInt((ObjectData obj) -> obj.alignment).reversed()); + return entries; + } + + StaticData(Encoding enc) { + this.enc=enc; + prefix=enc._code.entryClinitName()+"$DATA$"; + } + + static boolean isClass(Type t) { + return t instanceof TypeStruct ts && Parser.startsClzPrefix(ts._name); + } + + static CompUnit owner(CodeGen code,TypeStruct ts) { + return owner(code,ts._name.substring(Parser.CLZ.length())); + } + static CompUnit owner(CodeGen code,String name) { + if(name==null) return null; + CompUnit owner=null; + for( CompUnit cu : code._compunits.values() ) + if( cu._cname!=null && (name.equals(cu._cname) || name.startsWith(cu._cname+".")) && + (owner==null || cu._cname.length()>owner._cname.length()) ) owner=cu; + return owner; + } + + ObjectData object(Type t) { + Object key=isClass(t) ? ((TypeStruct)t)._name + : t instanceof TypeFunPtr fp ? functionSymbol(fp) : t; + ObjectData old=objects.get(key); + if( old!=null ) return old; + boolean clz=isClass(t); + if( clz && Parser.TYPES.get(((TypeStruct)t)._name) instanceof TypeStruct canonical ) t=canonical; + CompUnit owner=clz ? owner(enc._code,(TypeStruct)t) + : t instanceof TypeFunPtr fp ? owner(enc._code,enc._code._fidxs.owner(fp.fidx())) : null; + String symbol=clz ? ((TypeStruct)t)._name.substring(Parser.CLZ.length())+".$class" + : t instanceof TypeFunPtr fp ? functionSymbol(fp) : prefix+objects.size(); + ObjectData obj=new ObjectData(t,symbol,!clz && (!(t instanceof TypeStruct ts) || ts.isConstant()), + owner!=null && owner._src==null); + objects.put(key,obj); + // Register before following fields: class objects may refer to each other. + if( !obj.external && t instanceof TypeStruct ts ) + for( Field f : ts._fields ) + if( !f._extern && f._t.isConstant() ) target(f._t); + return obj; + } + + private static boolean pointerConstant(TypeMemPtr ptr) { + return ptr.notNull() && (isClass(ptr._obj) || (ptr._obj.isConstant() && (!ptr._obj.isAry() || ptr._obj.isConAry()))); + } + + private void target(Type t) { + if( t instanceof TypeMemPtr ptr && pointerConstant(ptr) ) object(ptr._obj); + if( t instanceof TypeFunPtr fp && fp.notNull() && + enc._code.link(fp)==null && !enc._code._externFunc.containsKey(fp.fidx()) ) + // A dead function still needs a unique non-null identity for comparisons. + object(fp); + } + + void write() { + ArrayList<Encoding.Relo> refs=new ArrayList<>(enc._bigCons.values()); + refs.sort(Comparator.comparingInt(r -> r._op._nid)); + for( Encoding.Relo ref : refs ) object(ref._t); + // Definitions belong to their compilation unit, even if every local use folded. + ArrayList<CompUnit> units=new ArrayList<>(enc._code._compunits.values()); + units.sort(Comparator.comparing(cu -> cu._cname==null ? "" : cu._cname)); + for( CompUnit cu : units ) if( cu._src!=null && cu._clz!=null ) object(cu._clz); + for( ObjectData obj : entries() ) { + if( obj.external ) continue; + BAOS bits=obj.readOnly ? enc._cpool : enc._sdata; + Encoding.padN(1<<obj.alignment,bits); + obj.offset=bits.size(); + if( obj.type instanceof TypeStruct ts ) writeStruct(obj,ts,bits); + else Encoding.addN(obj.alignment,obj.type,bits); + while( bits.size()<obj.offset+obj.size ) bits.write(0); + } + for( Encoding.Relo ref : refs ) { + ObjectData obj=object(ref._t); + ref._target=obj.offset; + ref._opStart=enc.opStart(ref._op); + } + } + + private void writeStruct(ObjectData obj, TypeStruct ts, BAOS bits) { + int[] layout=ts.layout(); + for( int idx : layout ) { + if( idx==ts._fields.length ) continue; + Field f=ts._fields[idx]; + if( f._extern ) continue; + int off=ts.offset(idx); + while( bits.size()<obj.offset+off ) bits.write(0); + if( f._fname.equals("[]") ) ((TypeConAry)f._t).write(bits); + else { + Type value=f._t.isConstant() ? f._t : f._t.makeZero(); + int log=f._t.log_size(); + Encoding.addN(log,value,bits); + if( (value instanceof TypeFunPtr fp && fp.notNull()) || + (value instanceof TypeMemPtr ptr && pointerConstant(ptr)) ) + relocations.add(new Relocation(obj,off,log,value)); + } + } + while( bits.size()<obj.offset+ts.size() ) bits.write(0); + } + + String functionSymbol(TypeFunPtr fp) { + String ext=enc._code._externFunc.get(fp.fidx()); + return ext!=null ? ext : enc._code._fidxs.symbol(fp.fidx()); + } + + String targetSymbol(Type value) { + if( value instanceof TypeMemPtr ptr ) return object(ptr._obj).symbol; + TypeFunPtr fp=(TypeFunPtr)value; + return enc._code.link(fp)==null && !enc._code._externFunc.containsKey(fp.fidx()) + ? object(fp).symbol : functionSymbol(fp); + } + + void link(int cpool, int sdata) { + for( Relocation rel : relocations ) { + long address; + if( rel.value instanceof TypeMemPtr ptr ) address=address(object(ptr._obj),cpool,sdata); + else { + TypeFunPtr fp=(TypeFunPtr)rel.value; + String ext=enc._code._externFunc.get(fp.fidx()); + FunNode fun=enc._code.link(fp); + if( ext!=null ) address=enc.externalAddress(ext); + else if( fun==null ) address=address(object(fp),cpool,sdata); + else { + if( !enc._code.owns(fun) ) throw new IllegalArgumentException("Unlinked function "+functionSymbol(fp)); + address=enc.opStart(fun); + } + } + // Emulator native-call sentinels are signed 32-bit addresses. + if( rel.logSize==2 && (address<Integer.MIN_VALUE || address>0xffff_ffffL) ) + throw new IllegalArgumentException("Function address outside 32 bits: "+address); + BAOS bits=rel.object.readOnly ? enc._cpool : enc._sdata; + int off=rel.object.offset+rel.offset; + for( int i=0; i<(1<<rel.logSize); i++ ) bits.buf()[off+i]=(byte)(address>>>(8*i)); + } + } + + int address(ObjectData obj,int cpool,int sdata) { + if( obj.external ) throw new IllegalArgumentException("Unlinked class object "+obj.symbol); + return obj.offset+(obj.readOnly ? cpool : sdata); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java new file mode 100644 index 00000000..9b4b0995 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java @@ -0,0 +1,57 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.Ary; + +// Reject invalid optimized programs before lowering erases their source-level +// obligations. Queue surviving CLInits during the same walk for Lower's Iter. +abstract class TypeCheck { + static void run(CodeGen code) { + // Check for class-init cycles first; such a cycle can prevent other + // things from resolving types. The dependency graph is temporary. + String clzInitCycle = new ClassInitDependencies(code).check(); + if( clzInitCycle != null ) + throw Parser.error(clzInitCycle,null); + + Ary<Node> errs = new Ary<>(Node.class); + code._stop.walk( n -> { + if( n.err() != null ) + errs.add(n); + if( n instanceof CLInitNode ) code.add(n); + return null; + }); + + if( !errs.isEmpty() ) { + // Prefer an originating error over a secondary error caused by a + // BOTTOM input. If all errors are downstream-poisoned, retain + // the old first-error behavior. + Node min = null; + for( Node n : errs ) + if( noBottomInputs(n) ) + { min = n; break; } + if( min==null ) + min = errs.at(0); + for( Node n : errs ) + if( n!=min && + noBottomInputs(n) && + ((n instanceof MemOpNode nmop && + min instanceof MemOpNode mmop && + nmop._loc.before(mmop._loc)) || + (n instanceof CFGNode ncfg && + min instanceof CFGNode mincfg && + ncfg.idepth() < mincfg.idepth())) ) + min = n; + if( code._obs != null ) code._obs.error(min, min.err().getMessage()); + throw min.err(); + } + } + + private static boolean noBottomInputs(Node n) { + for( Node def : n._inputs ) + if( def!=null && !(def instanceof FldOffNode) && def._type==Type.BOTTOM ) + return false; + return true; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/AddNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/AddNode.java new file mode 100644 index 00000000..8835208b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/AddNode.java @@ -0,0 +1,179 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; + +public class AddNode extends ArithNode { + @Override boolean allowFloat() { return true; } + public AddNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + public AddNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs, mode); } + + + @Override public String label() { return "Add"; } + @Override public String op() { return "+"; } + + @Override long doOp( long x, long y ) { return x + y; } + @Override double doOp(double x, double y ) { return x + y; } + @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { + // Fold ranges like {0-1} + {2-3} into {2-4}. + if( !overflow(x._min,y._min) && + !overflow(x._max,y._max) ) + return TypeInteger.make(x._min+y._min,x._max+y._max); + return TypeInteger.BOT; + } + + @Override + public Node idealize () { + // Settle the one-shot arithmetic mode before applying family-specific + // algebraic identities. + if( _mode==0 ) + return super.idealize(); + + Node lhs = in(1); + Node rhs = in(2); + if( rhs instanceof AddNode add && add.err()!=null ) + return null; + 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 && lhs._type.isa(compute()) ) + return lhs; + + // Add of same to a multiply by 2 + if( lhs==rhs && _mode==1 ) + return new ShlNode(null,lhs,CodeGen.CODE.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),_mode); + + // 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 copy(copy(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)) + if( spine_cmp(lhs,rhs,this) ) return swap12(); + Node n = phiCon(this,true); + return n == null ? super.idealize() : n; + } + + // 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.nIns()>=2 && 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( addDep(lhs.in(2))._type.isConstant() && rhs._type.isConstant() ) + return copy(lhs.in(1),copy(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 copy(copy(lhs.in(1),rhs).peephole(),lhs.in(2)); + + return super.idealize(); + } + + // 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)|(op y con)) ? + // 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,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 instanceof ParmNode) && 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() || lo instanceof ConFldOffNode ) return false; + if( hi._type.isConstant() || hi instanceof ConFldOffNode ) 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,_mode); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/AndNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/AndNode.java new file mode 100644 index 00000000..a737b703 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/AndNode.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +public class AndNode extends ArithNode { + public AndNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + + + @Override public String op() { return "&"; } + @Override long doOp( long x, long y ) { return x & y; } + @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { + // Sharpen allowed bits if either value is narrowed + long mask = x.mask() & y.mask(); + return mask < 0 ? TypeInteger.BOT : TypeInteger.make(0,mask); + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + + Type t1 = lhs._type; + Type t2 = rhs._type; + if( !(t1 instanceof TypeInteger && t2 instanceof TypeInteger t2i) ) + return null; // Malformed, e.g. (17 & 3.14) + + // And of -1. We do not check for (-1&x) because this will already + // canonicalize to (x&-1). We do not check for zero, because + // the compute() call will return a zero already. + if( t2i.isConstant() && t2i.value()==-1 && forceInt(lhs) ) + 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 super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new AndNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ArithNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ArithNode.java new file mode 100644 index 00000000..094b927f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ArithNode.java @@ -0,0 +1,208 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +public abstract class ArithNode extends Node implements ModeNode { + // Source location for late reported errors + Parser.Lexer _loc; + // Mode is 0 for unknown, 1 for int, 2 for flt + byte _mode; + + public ArithNode( Parser.Lexer loc, Node lhs, Node rhs) { super(null, lhs, rhs); _loc = loc; } + public ArithNode( Parser.Lexer loc, Node lhs, Node rhs, byte mode) { this(loc,lhs,rhs); _mode=mode; } + + abstract String op(); + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.open().n(in(1)).p(op()).n(in(2)).close(); + } + + abstract long doOp(long x, long y); + abstract TypeInteger doOp(TypeInteger x, TypeInteger y); + double doOp(double x, double y) { throw Utils.TODO(); } + + @Override public byte mode() { return _mode; } + @Override public Node setMode(byte mode) { + assert _mode==0 && (mode==1 || mode==2); + unlock(); // _mode participates in GVN hash and equality + _mode = mode; + return this; + } + static byte mode( Type t1, Type t2 ) { + if( t1 instanceof TypeFloat || t2 instanceof TypeFloat ) return 2; + if( t1 instanceof TypeInteger && t2 instanceof TypeInteger ) return 1; + if( t1 instanceof TypeMemPtr && t2 instanceof TypeMemPtr ) return 1; + if( t1 instanceof TypeFunPtr && t2 instanceof TypeFunPtr ) return 1; + return 0; + } + + // Int-only ops, such as &, | and shifts, can collapse to one input. If + // that input, or the live arms of a Phi input, are still mode-polymorphic + // ops, settle them before returning the value so the replacement remains + // at least as narrow as the int-only op. + static boolean forceInt(Node n) { + if( n instanceof ModeNode mode && mode.mode()==0 ) { + mode.setMode((byte)1); + n.setType(n.compute()); + // Mode changes remove n from GVN even if the caller's remaining + // conditions prevent replacing the int-only operation by n. + CodeGen.CODE.add(n); + } + return true; + } + + // Check that a settled mode agrees with all data inputs. Takes the Node + // instead of a varargs list both to avoid allocation and to keep this ready + // to become shared mode-aware Node behavior later. + static Parser.ParseException nodeErr(Node n, Parser.Lexer loc, String op, byte mode, boolean allowFloat) { + Type bad = null; + for( int i=1; i<n.nIns(); i++ ) { + Type t = n.in(i)._type; + if( t==null ) + return null; + if( t.isHigh() ) + return null; + // A weak input is not evidence of a bad mode while the graph is + // still being built. Loop Phis commonly await their backedge. + if( CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal() && t == Type.BOTTOM ) + return null; + bad = switch( t ) { + case TypeInteger ti -> mode==1 ? null : t; + case TypeMemPtr ti -> mode==1 ? null : t; + case TypeFunPtr ti -> mode==1 ? null : t; + case TypeFloat tf -> mode==2 ? null : t; + default -> t; + }; + if( bad != null ) break; + } + boolean unsupportedFloat = false; + if( !allowFloat ) + for( int i=1; i<n.nIns(); i++ ) + if( n.in(i)._type instanceof TypeFloat ) { + bad = n.in(i)._type; + unsupportedFloat = true; + break; + } + if( bad == null ) + return null; + if( unsupportedFloat || n instanceof BoolNode ) + return Parser.error("Cannot '"+op+"' "+bad,loc); + if( n.nIns()==3 ) + return Parser.error("Cannot "+n.in(1)._type+" "+op+" "+n.in(2)._type,loc); + return Parser.error("Cannot '"+op+"' "+bad,loc); + } + + // Arithmetic subclasses which have a floating-point interpretation override. + boolean allowFloat() { return false; } + + // Generic arithmetic op math: high returns high; low returns low; 2 + // constants fold; only 2 non-constants call specialized math. + @Override + public final Type compute() { + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==null || t2==null ) return Type.TOP; + if( t1.isHigh() || t2.isHigh() ) return Type.TOP; + if( t1==Type.BOTTOM || t2==Type.BOTTOM ) return Type.BOTTOM; + byte mode = _mode==0 ? mode(t1,t2) : _mode; + if( mode==0 ) + return CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal() ? Type.BOTTOM : Type.TOP; + if( mode==1 ) { + if( t1 instanceof TypeInteger x && + t2 instanceof TypeInteger y ) + return x.isConstant() && y.isConstant() + ? con(x,y) + : doOp(x,y).makeWide( (byte)Math.max(x._widen,y._widen) ); + return TypeInteger.BOT; + } + // FP handling + if( t1 instanceof TypeFloat f1 && + t2 instanceof TypeFloat f2 ) { + if( f1.isConstant() && f2.isConstant() ) + return TypeFloat.constant(doOp(f1.value(),f2.value())); + return t1.meet(t2); + } + return TypeFloat.F64; + } + + private TypeInteger con( Type t1, Type t2 ) { + return TypeInteger.constant( doOp( ((TypeInteger)t1).value(), ((TypeInteger)t2).value()) ); + } + + // Check for "op(Phi(con,x),Phi(con,y))" and push-up through the Phi. + // 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. + // Returns "Phi(op(con,con),op(x,y))". + + // Expected to be called after other ideal checks are done. + @Override public Node idealize() { + Type t1 = in(1)._type, t2 = in(2)._type; + + // Can we decide int vs flt? + if( _mode==0 ) { + byte mode = mode(t1,t2); + if( mode!=0 ) return setMode(mode).init(); + } + if( _mode==2 ) { + if( t1 instanceof TypeInteger ) return copy(new ToFloatNode(in(1)).peephole(),in(2)); + if( t2 instanceof TypeInteger ) return copy(in(1),new ToFloatNode(in(2)).peephole()); + } + + if( in(1) instanceof PhiNode lhs && + in(2) instanceof PhiNode rhs && + lhs.nIns() >= 2 && !lhs.inProgress() && + // Disallow with self-looping phi; these will collapse + (lhs.nIns() > 2 && lhs.in(2)!=lhs && rhs.nIns() > 2 && rhs.in(2)!=rhs) ) { + Node r = lhs.region(); + if( r==rhs.region() && !(r instanceof FunNode) && + // For loops, the backedge *must* fold away, lest we unroll forever + (!(r instanceof LoopNode) || isCon(lhs,rhs,2)) ) { + // Profit check: only 1 instance of `this` will remain, all the + // others will fold to constants. For loops, *all* must fold away + int cnt=0; + for( int i=1; i<lhs.nIns(); i++ ) + if( isCon(lhs,rhs,i) ) + cnt++; + if( lhs.nIns()-1 - cnt <= 1 ) { + // Profit! Push up-phi and fold + Node[] ns = new Node[lhs.nIns()]; + ns[0] = lhs.in(0); + for( int i=1; i<lhs.nIns(); i++ ) { + ns[i] = isCon(lhs,rhs,i) + ? ConstantNode.make(con(lhs.in(i)._type, rhs.in(i)._type)).peephole() + : copy(lhs.in(i), rhs.in(i)).peephole(); + } + String label = lhs._label==rhs._label ? lhs._label : lhs._label + rhs._label; + return new PhiNode(label, ns).peephole(); + } + } + } + + return null; + } + + static boolean isCon( PhiNode lhs, PhiNode rhs, int i ) { + return lhs.in(i)._type.isConstant() && rhs.in(i)._type.isConstant(); + } + + + 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 Parser.ParseException err() { + return nodeErr(this,_loc,op(),_mode,allowFloat()); + } + + @Override public boolean eq( Node n ) { return _mode==((ArithNode)n)._mode; } + @Override public int hash() { return _mode; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/BoolNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/BoolNode.java new file mode 100644 index 00000000..dd0bc996 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/BoolNode.java @@ -0,0 +1,188 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; +import java.util.HashMap; +import java.util.IdentityHashMap; +import static com.seaofnodes.simple.type.TypeInteger.*; + +abstract public class BoolNode extends Node implements ModeNode { + + // Mode is 0 for unknown, 1 for int, 2 for flt + byte _mode; + + public BoolNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + public BoolNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs); _mode = mode; } + + + abstract public String op(); // String opcode name + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.open().n(in(1)).p(op()).n(in(2)).close(); + } + + @Override + public TypeInteger compute() { + Type t1 = in(1)._type; + Type t2 = in(2)._type; + if( t1==null || t2==null ) + return BOOL; + // Exactly equals? + if( t1.isHigh() || t2.isHigh() ) + return (TypeInteger)BOOL.dual(); + if( in(1)==in(2) ) + // LT fails, both EQ and LE succeed + return this instanceof LT ? FALSE : TRUE; + byte mode = _mode==0 ? ArithNode.mode(t1,t2) : _mode; + if( mode==0 ) + return BOOL; + // Handle integer compares + if( mode==1 ) { + if( t1 instanceof TypeInteger i1 && + t2 instanceof TypeInteger i2 ) + return doOp(i1,i2); + return BOOL; + } + // Floating compare. Mixed inputs are converted in idealize(). + 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(); } + public boolean isFloat() { assert _mode!=0; return _mode==2; } + @Override public byte mode() { return _mode; } + @Override public Node setMode(byte mode) { + assert _mode==0 && (mode==1 || mode==2); + unlock(); // _mode participates in GVN hash and equality + _mode = mode; + return this; + } + + @Override + public Node idealize() { + // Compare of same + if( in(1)==in(2) ) + return this instanceof LT ? CodeGen.CODE.ZERO : ConstantNode.make(TRUE); + // Can we decide int vs flt? + Type t1 = in(1)._type, t2 = in(2)._type; + if( t1==null || t2==null ) + return null; + if( _mode==0 ) { + byte mode = ArithNode.mode(t1,t2); + if( mode!=0 ) return setMode(mode).init(); + } + if( _mode==2 ) { + if( t1 instanceof TypeInteger ) return copy(new ToFloatNode(in(1)).peephole(),in(2)); + if( t2 instanceof TypeInteger ) return copy(in(1),new ToFloatNode(in(2)).peephole()); + } + + + // 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 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 ((phi cons) cmp con) ? + // Do we have ((phi cons) cmp (phi cons)) ? + // Push the compare up through the phi: (phi con0 cmp con, con1 cmp con...) + Node phicon = AddNode.phiCon(this,this instanceof EQ); + if( phicon!=null ) return phicon; + + return null; + } + + @Override public boolean eq( Node n ) { return _mode==((BoolNode)n)._mode; } + @Override public int hash() { return _mode; } + @Override public Parser.ParseException err() { + return ArithNode.nodeErr(this,null,op(),_mode,!(this instanceof ULT)); + } + + public static class EQ extends BoolNode { + public EQ(Node lhs, Node rhs) { super(lhs,rhs); } + public EQ(Node lhs, Node rhs, byte mode) { super(lhs,rhs, mode); } + + 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; + } + @Override boolean doOp(double lhs, double rhs) { return lhs==rhs; } + Node copy(Node lhs, Node rhs) { return new EQ(lhs,rhs,_mode); } + } + + public static class NE extends BoolNode { + public NE(Node lhs, Node rhs) { super(lhs,rhs); } + public NE(Node lhs, Node rhs, byte mode) { super(lhs,rhs,mode); } + + 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; + } + @Override boolean doOp(double lhs, double rhs) { return lhs!=rhs; } + Node copy(Node lhs, Node rhs) { return new NE(lhs,rhs,_mode); } + } + + public static class LT extends BoolNode { + public LT(Node lhs, Node rhs) { super(lhs,rhs); } + public LT(Node lhs, Node rhs, byte mode) { super(lhs,rhs, mode); } + + public String op() { return "<" ; } + TypeInteger doOp(TypeInteger i1, TypeInteger i2) { + if( i1._max < i2._min ) return TRUE; + if( i1._min >= i2._max ) return FALSE; + return BOOL; + } + @Override boolean doOp(double lhs, double rhs) { return lhs<rhs; } + Node copy(Node lhs, Node rhs) { return new LT(lhs,rhs,_mode); } + } + + public static class LE extends BoolNode { + public LE(Node lhs, Node rhs) { super(lhs,rhs); } + public LE(Node lhs, Node rhs, byte mode) { super(lhs,rhs,mode); } + + public String op() { return "<="; } + TypeInteger doOp(TypeInteger i1, TypeInteger i2) { + if( i1._max <= i2._min ) return TRUE; + if( i1._min > i2._max ) return FALSE; + return BOOL; + } + @Override boolean doOp(double lhs, double rhs) { return lhs<=rhs; } + Node copy(Node lhs, Node rhs) { return new LE(lhs,rhs,_mode); } + } + + // 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,(byte)1); } + public ULT(Node lhs, Node rhs, byte mode) { + super(lhs,rhs,(byte)1); + assert mode==1; + } + + public String op() { 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/chapter-modules/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java new file mode 100644 index 00000000..df9e4fff --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java @@ -0,0 +1,265 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.util.Utils; + +import java.util.BitSet; +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + * A Phi for bulk memory. It covers all aliases except {@link #_aliases}; + * those aliases must be represented by parallel precise MemPhis at the same + * program slice. + */ +public class BulkMemPhiNode extends PhiNode { + + private static final BitSet EMPTY = new BitSet(); + + // Excluded Aliases; empty means NO exclusions. + // Directly visible by design. Empty sets share EMPTY; splitAlias performs + // copy-on-first-write and removes the node from GVN before mutation. + public BitSet _aliases; + + public BulkMemPhiNode(String label, Node... inputs) { + this(label,EMPTY,inputs); + } + + private BulkMemPhiNode(String label, BitSet aliases, Node... inputs) { + super(label, inputs); + _aliases = aliases; + } + + public BulkMemPhiNode(BulkMemPhiNode phi) { + super(phi,phi._label); + _aliases = phi._aliases.isEmpty() ? EMPTY : (BitSet)phi._aliases.clone(); + } + + public BulkMemPhiNode(RegionNode r, Node sample) { + super(r,sample); + _aliases = EMPTY; + } + + public boolean isSplit(int alias) { return _aliases.get(alias); } + + + @Override + public Type compute() { + Type t = super.compute(); + return t instanceof TypeMem mem ? mem : + t==Type.BOTTOM ? TypeMem.BOT : + TypeMem.TOP; + } + + @Override + public Node idealize() { + Node progress = super.idealize(); + if( progress != null ) return progress; + if( ((RegionNode)region()).inProgress() || region().nIns()<=1 ) + return null; // Input is in-progress + + // "Peek through" a MemMerge that covers this alias set on its default + for( int i=1; i<nIns(); i++ ) + if( in(i) instanceof MemMergeNode mmm && canPeek(mmm) ) { + setDef(i,mmm.in(1)); + return this; + } + + // If any input or output uses or defines a specific alias we cover, + // make a private MemPhi for it, and add it to the excluded list. + for( int i=1; i<nIns(); i++ ) { + int alias = inputAlias(in(i)); + if( alias!=0 ) + return slice(alias); + } + + for( int i=0; i<nOuts(); i++ ) { + int alias = outputAlias(out(i)); + if( alias!=0 ) + return slice(alias); + } + + return null; + } + + static public boolean checkMem(Node n, int alias) { + for( Node m : n._inputs ) + // Bulk memory supplies this alias + if( m instanceof BulkMemPhiNode bulk ) { + if( bulk._aliases.get(alias) ) + return false; + } else if( m instanceof MemMergeNode mmm && + mmm.in(1) instanceof BulkMemPhiNode bulk && + bulk._aliases.get(alias) && + (alias >= mmm.nIns() || mmm.in(alias)==null) ) + return false; + return true; + } + + + // True if the MemMerge has no precise slice still covered by this Phi. + private boolean canPeek(MemMergeNode mmm) { + return missingAlias(mmm)==0; + } + + // Return an alias required by an input but still covered by this Phi, or 0. + private int inputAlias(Node n) { + return switch(n) { + case ParmNode p -> { assert p._idx==1; yield 0; } + case ProjNode proj -> 0; + case ConstantNode con -> { + assert !(con._con instanceof TypeMem tmem && tmem._alias != 1); + yield 0; + } + case MemMergeNode mmm -> missingAlias(mmm); + case MemOpNode mem -> unsplit(mem._alias); + case BulkMemPhiNode bulk -> missingAlias(bulk); + default -> throw Utils.TODO("Should not reach here"); + }; + } + + // Return an alias required by a user but still covered by this Phi, or 0. + private int outputAlias(Node use) { + // This decision depends backwards on an immediate user's alias/set. + // Normal graph-neighbor enqueueing flows from defs to uses, so retain + // an explicit forward dependency to revisit this Phi when the user + // sharpens. + use = addDepForwards(use); + return switch(use) { + case ScopeNode scope -> 0; + case ParmNode parm -> 0; + case MemMergeNode mmm -> missingAlias(mmm); + case BulkMemPhiNode bulk -> missingAlias(bulk); + case MemOpNode mem -> unsplit(mem._alias); + case MemPhiNode phi -> unsplit(phi._alias); + case EscapeNode esc -> esc.pub()==this ? unsplit(esc.fld()._alias) : 0; + default -> { + assert use instanceof ReturnNode || use instanceof CallNode || use instanceof CLInitNode + : "Unexpected bulk-memory user "+use.getClass().getSimpleName()+"#"+use._nid+": "+use; + yield 0; + } + }; + } + + // Alias zero/one is bulk, and an already excluded precise alias cannot + // trigger another split of this Phi. + private int unsplit(int alias) { + if( alias <= 1 ) return 0; + assert !_aliases.get(alias); + return alias; + } + + // First alias excluded by the neighboring bulk Phi but not by this one. + private int missingAlias(BulkMemPhiNode bulk) { + for( int alias = bulk._aliases.nextSetBit(0); alias >= 0; alias = bulk._aliases.nextSetBit(alias+1) ) + if( !_aliases.get(alias) ) + return alias; + return 0; + } + + // First explicit MemMerge slice still covered by this Phi. A user's slice + // may point to this Phi after a precise Phi folds; splitting is still valid. + private int missingAlias( MemMergeNode mmm ) { + for( int alias=2; alias<mmm.nIns(); alias++ ) + if( mmm.in(alias)!=null && + mmm.alias(alias)!=mmm.in(1) && + !_aliases.get(alias) ) + return alias; + return 0; + } + + // Slice out given alias + private MemMergeNode slice( int alias ) { + assert !_aliases.get(alias); + // Inputs are the same; MemPhi will sharpen his own inputs. If a + // parallel precise Phi already exists, reuse it instead of building a + // duplicate for the same Region/alias pair. + Node mem = precisePhi(alias); + assert ((TypeMem)mem._type)._alias==alias; + + BitSet aliases = ((BitSet)_aliases.clone()); + aliases.set(alias); + BulkMemPhiNode bphi = new BulkMemPhiNode(_label,aliases); + for( int i=0; i<nIns(); i++ ) + bphi.addDef(in(i)); + // Installing bphi creates a new bulk user with a different exclusion + // set. Queue the original memory inputs before bphi itself can peep + // away and hide those neighbor relationships. + for( int i=1; i<bphi.nIns(); i++ ) + CodeGen.CODE.add(bphi.in(i)); + // Do not peephole bphi here, as BulkMemPhi can recursively start a + // second bulk rewrite before this one has finished. Let the worklist + // discover any further splits + Node bulk = bphi.init(); + CodeGen.CODE.add(bulk); + CodeGen.CODE.add(mem); + return aggregate(bulk,alias,mem); + } + + // Build aggregate memory over a bulk Phi, preserving every precise slice + // which the bulk has already excluded. The named alias is newly supplied + // (or replaces its old slice). + MemMergeNode aggregate(Node bulk, int alias, Node precise) { + MemMergeNode mmm = new MemMergeNode(null,bulk); + for( int old = _aliases.nextSetBit(0); old >= 0; old = _aliases.nextSetBit(old+1) ) + { assert old != alias; mmm.alias(old,precisePhi(old));} + mmm.alias(alias,precise); + assert checkMerge(mmm); + mmm.init(); + // Slicing changes representation, not the set of values in memory. + // Newly exposed precise inputs can carry conservative bulk escape + // summaries; do not let those invent escapes absent from this Phi. + TypeMem old = (TypeMem)_type; + TypeMem mem = (TypeMem) mmm._type; + mmm._type = TypeMem.make(mem._alias,mem._t,mem._one,mem._clz, + mem._final,old._escFs,old._escAs); + return mmm; + } + + // Find or make the precise Phi parallel to this bulk Phi at the same merge. + private Node precisePhi(int alias) { + MemPhiNode mphi = _findPhi(alias); + if( mphi!=null ) return mphi; + + mphi = new MemPhiNode("$"+alias,alias); + mphi.addDef(region()); + // Due to cycles, must set before calling peephole + mphi.setType(TypeMem.BOT.makeFrom(alias)); + for( int i=1; i<nIns(); i++ ) + mphi.addDef(CodeGen.CODE.add(preciseInput(in(i),alias))); + return CodeGen.CODE.add(mphi); + } + + MemPhiNode _findPhi(int alias) { + for( Node use : region().outs() ) + if( use instanceof MemPhiNode mphi && mphi._alias==alias ) + return mphi; + return null; + } + + // Select an alias already split out of a predecessor's bulk memory. + private Node preciseInput(Node mem, int alias) { + if( mem instanceof MemMergeNode mmm ) + return mmm.alias(alias); + if( mem instanceof BulkMemPhiNode bulk && bulk.isSplit(alias) ) + return bulk.precisePhi(alias); + return mem; + } + + // Every alias excluded by a bulk default must be supplied explicitly. + static boolean checkMerge(MemMergeNode mmm) { + if( !(mmm.in(1) instanceof BulkMemPhiNode bulk) ) return true; + for( int alias = bulk._aliases.nextSetBit(0); alias >= 0; alias = bulk._aliases.nextSetBit(alias+1) ) + if( alias >= mmm.nIns() || mmm.in(alias)==null ) + return false; + return true; + } + + @Override public boolean eq(Node n) { + return _aliases.equals(((BulkMemPhiNode)n)._aliases) && super.eq(n); + } + + @Override int hash() { return _aliases.hashCode(); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CFGNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CFGNode.java new file mode 100644 index 00000000..4f225fbe --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CFGNode.java @@ -0,0 +1,303 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; + +import java.util.BitSet; +import java.util.IdentityHashMap; + +/** 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. + * + * Start - Block head; has constants and trailing Funs + * Region - Block head; has Phis. Includes Fun and Parms. + * CallEnd - Block head; followed by CProj and Proj; also linked RPC Parms + * CProj - Block head + * + * If - Block tail; followed by CProj same block + * Call - Block tail; followed by CallEnd and linked Funs + * New/intrinsic - If followed by CProj: block tail else mid-block; followed by Proj + * + */ +public abstract class CFGNode extends Node { + + public CFGNode(Node... nodes) { super(nodes); } + public CFGNode(CFGNode cfg) { + super(cfg); + if( cfg != null ) { + _idepth = cfg._idepth; + _idepthVersion = cfg._idepthVersion; + _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; + } + + // Used by the encoding / final BB layout + public CFGNode uctrlSkipEmpty() { + CFGNode x = this, y; + while( x.nOuts() == 1 && (y=x.uctrl())!=null && !(y instanceof CallNode) ) // Skip empty blocks + x = y; + return x; + } + + // ------------------------------------------------------------------------ + /** + * 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 char _idepth; + public char _idepthVersion; + public int idepth() { return validIDepth() ? _idepth : cacheIDepth(idom().idepth()+1); } + // Zero depth means uncached. Check before narrowing so overflow cannot wrap. + final int cacheIDepth(int depth) { + assert 0 <= depth && depth <= Character.MAX_VALUE : "Dominator depth exceeds 65535"; + _idepthVersion = CodeGen.CODE.iDepthVersion(); + return _idepth = (char)depth; + } + + final boolean validIDepth() { return _idepth!=0 && _idepthVersion==CodeGen.CODE.iDepthVersion(); } + + + // 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 domLCA(CFGNode rhs, Node dep) { + if( rhs==null ) return this; + CFGNode lhs = this; + while( lhs != rhs ) { + if( lhs==null || rhs==null ) return null; + int comp = lhs.idepth() - rhs.idepth(); + if( comp >= 0 ) lhs = (dep==null ? lhs : dep.addDep(lhs)).idom(); + if( comp <= 0 ) rhs = (dep==null ? rhs : dep.addDep(rhs)).idom(); + } + return lhs; + } + + // Safe read of the control input of a data node. + public static CFGNode safeCFG(Node n) { + if( n==null ) return null; + if( !(n.in(0) instanceof CFGNode cfg) ) return null; + return cfg._safeCFG(); + } + private CFGNode _safeCFG() { + return switch(this) { + case CLInitNode init when init.bypass() -> init.cfg0()._safeCFG(); + case CallNode call when call.cend()._folding -> call.cfg0()._safeCFG(); + case CallEndNode cend when cend._folding -> cend.cfg(1)._safeCFG(); + case FunNode fun when fun._folding -> fun.cfg(1)._safeCFG(); + case ReturnNode ret when ret.fun()._folding -> ret.cfg0()._safeCFG(); + default -> this; + }; + } + + // True when this node dominates `sub` in the current idom tree. + public boolean dominates(CFGNode sub, Node dep) { + for( CFGNode cfg=sub; cfg!=null; cfg=cfg.idom() ) + if( cfg == this ) + return true; + else if( dep != null ) dep.addDep(cfg); + return false; + } + + public static CFGNode deepest(CFGNode a, CFGNode b) { + if( a == null ) return b; + if( b == null ) return a; + return b.idepth() > a.idepth() ? b : a; + } + + public static CFGNode earlyCFG(Node n) { + return earlyCFG(n,new IdentityHashMap<>(),new BitSet()); + } + + public static CFGNode earlyCFG(Node n, IdentityHashMap<Node,CFGNode> cache, BitSet active) { + if( n == null ) return null; + CFGNode cached = cache.get(n); + if( cached != null ) return cached; + if( active.get(n._nid) ) + return safeCFG(n); + active.set(n._nid); + CFGNode early = null; + for( Node x : n._inputs ) + early = deepest(early,defCFG(x,cache,active)); + active.clear(n._nid); + cache.put(n,early); + return early; + } + + private static CFGNode defCFG(Node n, IdentityHashMap<Node,CFGNode> cache, BitSet active) { + if( n == null || n.isConst() ) return null; // no impact on early cfg + return switch( n ) { + case PhiNode phi -> phi.region(); + case CFGNode cfg -> cfg; + case ProjNode projNode -> safeCFG( n ); + default -> earlyCFG( n, cache, active ); + }; + } + + public boolean sameFun(CFGNode that) { + FunNode fun = fun(); + return fun != null && fun == that.fun(); + } + + // Find nearest enclosing FunNode + public FunNode fun() { + CFGNode cfg = this; + while( !(cfg instanceof FunNode fun) ) { + if( cfg==null ) return null; + cfg = cfg.idom(); + } + return fun; + } + + // ------------------------------------------------------------------------ + // Loop nesting + public LoopNode loop() { return _ltree._head; } + public int loopDepth() { return _ltree==null ? 0 : _ltree.depth(); } + + public LoopTree _ltree; + public int _pre; // Pre-order numbers for loop tree finding + public static class LoopTree { + public LoopTree _par; + public final LoopNode _head; + public int _depth; + LoopTree(LoopNode head) { _head = head; } + @Override public String toString() { return label(_head); } + public 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 contains the function bodies at depth 0; real loops add depth. + public void buildLoopTree( Ary<FunNode> funs, StopNode stop) { + _ltree = stop._ltree = CodeGen.CODE.XCTRL._ltree = new LoopTree((StartNode)this); + Ary<FunNode> work = new Ary<>(FunNode.class); + + BitSet post = new BitSet(); + int pre = 1; + _pre = pre++; + for( FunNode fun : funs ) { + if( fun!=null && !fun.isDead() && !fun._type.isHigh() ) { + work.push(fun); + if(fun.in(1) instanceof StartCUNode start && start._pre==0 ) { + start._pre = pre++; + start._ltree = start.cfg(1)._ltree = _ltree; + post.set(start._nid); + } + }; + } + + for( int i=0; i<work._len; i++ ) + pre = work.at(i)._bltWalk(pre,work.at(i),stop, post, work); + } + + int _bltWalk( int pre, FunNode fun, StopNode stop, BitSet post, Ary<FunNode> work ) { + // Pre-walked? + if( _pre!=0 ) return pre; + _pre = pre++; + // Seed the function's outer tree before walking its body. Synthetic + // exits can be added before the post-order walk reaches the entry. + if( this instanceof FunNode ) _ltree = stop._ltree; + if( this instanceof ReturnNode ) { + _ltree = fun._ltree; + post.set(_nid); + return pre; // Stop/Start links describe the outside world, not a loop. + } + // Pre-walk + for( Node use : _outputs ) + if( use instanceof CFGNode usecfg ) { + if( skip( usecfg ) ) { + if( usecfg._pre==0 && usecfg instanceof FunNode fun2 ) work.add( fun2 ); + } else + pre = usecfg._bltWalk( pre, use instanceof FunNode fuse ? fuse : fun, stop, post, work ); + } + + // 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; + } + + public 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); + } + + public static String label( CFGNode target ) { + return switch(target) { + case StartNode start -> start.label(); + case FunNode fun -> fun.label(); + case LoopNode loop -> "LOOP"+target._nid; + default -> "L"+target._nid; + }; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CLInitNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CLInitNode.java new file mode 100644 index 00000000..0a0de3cf --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CLInitNode.java @@ -0,0 +1,165 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.CompUnit; +import com.seaofnodes.simple.type.*; +import java.util.*; + +// Track class initializers - the code just after this point expects the named +// class to be initialized. If this code path dies, then the dependency dies +// also - dead code does not force class init. +// +public class CLInitNode extends CFGNode implements MultiNode { + public Parser.Lexer _loc; // For error reporting + + // Inputs: control, whole memory, class pointer, loaded $init flag + // Results: control, whole memory, known-true flag alias memory. + // For field references, the ptr might be the dst clz ptr or not. + public CLInitNode(Parser.Lexer loc, Node ctrl, Node mem, Node ptr, Node ready) { + super(ctrl,mem,ptr,ready); + _loc = loc; + } + + + public CLInitNode(CLInitNode init) { + super(init); + _loc = init._loc; + } + + public Node mem() { return in(1); } + public Node ptr() { return in(2); } + public Node ready() { return in(3); } + + // The field/alias can be unresolved until forward references are loaded. + public Field flag() { + return ptr()._type instanceof TypeMemPtr tmp && Parser.startsClzPrefix(tmp._obj._name) + ? tmp._obj.field("$init") : null; + } + @Override public String label() { return "CLInit"; } + + // Best effort dst compunit, based on ptr - or null + public CompUnit cu() { + if( !(ptr()._type instanceof TypeMemPtr ptr && Parser.startsClzPrefix(ptr._obj._name)) ) + return null; + // Convert "class:sys.ary" into "sys/ary" and lookup CompUnit + String fname = ptr._obj._name.substring(6).replace(".","/"); + return CodeGen.CODE._compunits.get(fname); + } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + FunNode fun = fun(); + CompUnit cu = cu(); + return p.p("CLInit ").p(fun._name).p(" -> ").p(cu==null ? "---" : cu._cname); + } + + // Block head is Start, Region, CProj, but not e.g. If, Return, Stop + public boolean blockHead() { return true; } + + private boolean instance() { + return ptr()._type==Type.NIL || + (ptr()._type instanceof TypeMemPtr tmp && tmp._obj!=TypeStruct.BOT && + !tmp._obj.isHigh() && !tmp._obj._open && !tmp._obj._fref && + !Parser.startsClzPrefix(tmp._obj._name)); + } + + public boolean bypass() { return instance() || ready()._type==TypeInteger.TRUE; } + + @Override public Type compute() { + if( in(0)._type.isHigh() || ptr()._type.isHigh() ) + return TypeTuple.make(Type.XCONTROL,TypeMem.TOP,TypeMem.TOP); + if( instance() ) + return TypeTuple.make(in(0)._type,mem()._type,mem()._type); + Field flag = flag(); + Type slice = flag==null ? TypeMem.BOT : TypeMem.make(flag._alias,TypeInteger.TRUE); + // The initializer may write arbitrary public memory. Only its flag + // has a guaranteed value on the normal continuation. + Type bulk = ready()._type.isHigh() ? TypeMem.TOP : + ready()._type==TypeInteger.TRUE ? mem()._type : TypeMem.BOT; + return TypeTuple.make(in(0)._type,bulk,slice); + } + + @Override public Node pcopy(int idx, Node dep) { + dep.addDep(ptr()); + dep.addDep(ready()); + if( instance() ) return idx==0 ? in(0) : mem(); + if( ready()._type!=TypeInteger.TRUE ) return null; + if( idx==0 ) return in(0); + if( idx==1 ) return mem(); + // Already established on the incoming path; this is a memory fact, + // not a new store to the class object. + return CodeGen.CODE.con(((TypeTuple)_type)._types[2]); + } + + @Override public Node idealize() { + if( CodeGen.CODE._phase!=CodeGen.Phase.Lower || bypass() || in(0)._type.isHigh() ) return null; + if( CodeGen.CODE._midAssert ) return this; + // Until entry arguments become arrays, ordinary initializers receive zero. + return expand(CodeGen.CODE.con(TypeInteger.ZERO)); + } + + // Called only after cycle/type checking. The initialized arm skips the + // call; both arms rejoin with whole memory and a known-true flag slice. + private Node expand(Node arg) { + FunNode target = CodeGen.CODE.link(cu()); + assert target!=null && !target.isDead(); + keep(); + Node ctrl = in(0); + Node memory = mem(); + + // A child body may assume its parent has finished. Establish that + // requirement first, then test the child's flag in the resulting memory. + CompUnit parent = cu()._par; + if( parent!=null ) { + Node ptr = CodeGen.CODE.con(TypeMemPtr.make((byte)2,parent._clz,true)); + Field flag = parent._clz.field("$init"); + Node off = CodeGen.CODE.con(parent._clz.offset(parent._clz.find("$init"))); + Node parentReady = CodeGen.CODE.add(new LoadNode(_loc,"$init",flag._alias,ctrl,memory,ptr,off).init()); + CLInitNode init = CodeGen.CODE.add(new CLInitNode(_loc,ctrl,memory,ptr,parentReady).init()); + ctrl = CodeGen.CODE.add(new CProjNode(init,0,ScopeNode.CTRL).init()); + memory = CodeGen.CODE.add(new ProjNode(init,1,ScopeNode.MEM0).init()); + } + + Node childOff = CodeGen.CODE.con(cu()._clz.offset(cu()._clz.find("$init"))); + // Read the child's flag after establishing its parent's memory effects. + Node ready = CodeGen.CODE.add(new LoadNode(_loc,"$init",flag()._alias,ctrl,memory,ptr(),childOff).init()); + + IfNode iff = CodeGen.CODE.add(new IfNode(ctrl,ready).init()); + Node done = CodeGen.CODE.add(new CProjNode(iff,0,"True").init()); + Node cold = CodeGen.CODE.add(new CProjNode(iff,1,"False").init()); + + Node fptr = new FunPtrNode(target.sig(),CodeGen.CODE._start,target.ret()).peephole(); + CallNode call = CodeGen.CODE.add(new CallNode(_loc,cold,memory,ptr(),arg,fptr).init()); + CallEndNode end = new CallEndNode(call,TypeRPC.constant(CodeGen.CODE.rpc(_loc._cu._cname))); + // This is a lowered call, after interprocedural analysis. Do not add + // new parameter flows to a body already optimized by SCCP. + CodeGen.CODE.add(end.init()); + Node called = CodeGen.CODE.add(new CProjNode(end,0,ScopeNode.CTRL).init()); + Node after = CodeGen.CODE.add(new ProjNode(end,1,ScopeNode.MEM0).init()); + RegionNode region = CodeGen.CODE.add(new RegionNode(_loc,null,done,called).init()); + Node merged = CodeGen.CODE.add(new BulkMemPhiNode(ScopeNode.MEM0,region,memory,after).init()); + + // Replace the individual outputs; no tuple-pack node or second + // representation of CLInit is needed while the projections collapse. + for( Node use : _outputs.asAry() ) { + if( use==null ) continue; // Our temporary keep. + assert use instanceof Proj; + int idx = ((Proj)use).idx(); + use.subsume(idx==0 ? region : idx==1 ? merged : CodeGen.CODE.con(((TypeTuple)_type)._types[2])); + } + CodeGen.CODE.invalidateIDepthCaches(); + return unkeep(); // Iter removes this now-unused node. + } + + // Resolve a precise flag slice even when the parser did not yet know the + // class/alias and could only install the whole-memory projection. + static Node slice(Node mem, int alias, Node dep) { + if( !(mem instanceof ProjNode proj && proj._idx==1 && proj.in(0) instanceof CLInitNode init) ) + return mem; + dep.addDep(init.ptr()); + Field flag = init.flag(); + if( flag==null || flag._alias!=alias ) return mem; + return new ProjNode(init,2,Parser.memName(alias)).peephole(); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CProjNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CProjNode.java new file mode 100644 index 00000000..2bd48d92 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CProjNode.java @@ -0,0 +1,73 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeTuple; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class CProjNode extends CFGNode implements Proj { + + // Which slice of the incoming multipart value + public int _idx; + + // Debugging label + public 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 protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p(_label); } + + @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] : (t.isHigh() ? Type.TOP : Type.BOTTOM); + } + + @Override + public Node idealize() { + if( ctrl()._type instanceof TypeTuple tt ) { + if( tt._types[_idx]==Type.XCONTROL ) + return CodeGen.CODE.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() != null && addDep(iff.pred()) 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,this); + } + + // Only called during basic-block layout, inverts a T/F CProj + public void invert() { + _label = _idx == 0 ? "False" : "True"; + _idx = 1-_idx; + } + + @Override + public boolean eq( Node n ) { return _idx == ((CProjNode)n)._idx; } + + @Override + int hash() { return _idx; } + + @Override public int idx() { return _idx; } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallEndMach.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallEndMach.java new file mode 100644 index 00000000..05707132 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallEndMach.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.util.SB; + +public class CallEndMach extends CallEndNode implements MachNode { + public CallEndMach( CallEndNode n ) { super(n); } + // MachNode specifics, shared across all CPUs + public int _xslot; + private RegMask _retMask; + private RegMask _kills; + public void cacheRegs(CodeGen code) { + // Return mask depends on TFP (either GPR or FPR) + _retMask = code._mach.retMask(call().tfp()); + // Kill mask is all caller-saves, and any mirror stack slots for args + // in registers. + RegMaskRW kills = (call().external() ? code._cCallerSave : code._callerSave).copy(); + // Start of stack slots + int maxReg = code._mach.regs().length; + // Incoming function arg slots, all low numbered in the RA + int fslot = fun()._maxArgSlot; + // Killed slots for this calls outgoing args + int xslot = call().external() + ? code._mach.maxArgSlot(call().tfp(),code._cCallingConv) + : code._mach.maxArgSlot(call().tfp()); + _xslot = (maxReg+fslot)+xslot; + for( int i=0; i<xslot; i++ ) + kills.set((maxReg+fslot)+i); + _kills = kills; + } + public String op() { return "cend"; } + public RegMask regmap(int i) { return null; } + public RegMask outregmap() { return null; } + public RegMask outregmap(int idx) { return idx==2 ? _retMask : null; } + public RegMask killmap() { return _kills; } + public void encoding( Encoding enc ) { } + public void asm(CodeGen code, SB sb) { } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallEndNode.java new file mode 100644 index 00000000..4422265f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -0,0 +1,321 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + * CallEnd + */ +public class CallEndNode extends CFGNode implements MultiNode { + + // When set true, this Call/CallEnd/Fun/Return is being trivially inlined + boolean _folding; + // +2: must clone, so await all cleanup but OK + // +1: trivial now, so can inline without cloning and without waiting for cleanup + // 0: take a look, + // -1: somethings wrong, but might improve + // -2: never and stop asking, + private byte _inline; + public final TypeRPC _rpc; + + public CallEndNode(CallNode call, TypeRPC rpc) { + super(new Node[]{call}); + _rpc = rpc; + } + public CallEndNode(CallEndNode cend) { super(cend); _rpc = cend._rpc; } + public CallEndNode(TypeRPC rpc) { + super(new Node[0]); + _rpc = TypeRPC.constant(CodeGen.CODE._rpcs.next(rpc.rpc())); + } + private CallEndNode(int nIns, TypeRPC rpc) { + super(new Node[nIns]); + _rpc = rpc; + } + + + @Override public boolean blockHead() { return true; } + + public CallNode call() { return (CallNode)in(0); } + public boolean folding() { return _folding; } + + @Override public CFGNode idom(Node dep) { + // Folding the idom is the one inlining Return + return _folding ? cfg(1) : super.idom(dep); + } + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + p.p("cend( "); + p.p( in(0) instanceof CallNode ? "Call, " : "----, "); + for( int i=1; i<nIns()-1; i++ ) + p.n(in(i)).p(","); + return p.unchar(',').unchar(' ').close(); + } + + @Override + public Type compute() { + if( !(in(0) instanceof CallNode call) || call._type != Type.CONTROL ) + return TypeTuple.STATE.dual(); + // Grab the TFP and use the functions declared return type. + // If the call.fptr() is a FRef, return will be BOTTOM. + Type ftype = addDep(call.fptr())._type; + if( !(ftype instanceof TypeFunPtr tfp) ) + return ftype.isHigh() ? TypeTuple.STATE.dual() : TypeTuple.STATE; + // Mid-fold, just take the one single callers' return type + if( _folding ) { + TypeTuple tt = (TypeTuple)in(1)._type; + return tt.makeFrom(2,tt.ret()); + } + + // Here, if I can figure out I've linked *all* callers, then I can meet + // across the linked returns and join with the function return type. + + // External functions have an exact FIDX but deliberately no local + // FunNode/Return edge. They conservatively crush public memory and + // produce their declared return type. A missing non-external target + // is still an unresolved/error call. + int externs = 0; + // The signature bounds widening (e.g. an i32 result stays i32). + // Use the callee signature, not FunPtrNode's live recursive range. + Type bound = tfp.ret(); + if( !XInt.isHigh(tfp.fidxs()) ) { + bound = Type.TOP; + for( int fidx = XInt.next(tfp.fidxs(),0); fidx >= 0; fidx = XInt.next(tfp.fidxs(),fidx) ) { + if( CodeGen.CODE.externFunc(fidx)!=null ) + externs++; + FunNode fun = CodeGen.CODE._link(fidx); + bound = bound.meet(fun==null ? tfp.ret() : addDep(fun).sig().ret()); + } + } + boolean hasExtern = externs != 0; + + // If we have an error-call, e.g. wrong args, then we never link. + // Pre-Opto this looks like a missing target fcn, and we assume it will + // appear later - meanwhile, we use a conservative approx of memory + // effects. + if( (nIns()-1)+externs < tfp.nfcns() ) { + // If during Opto, assume call won't be called and thus won't return anything. + // Lowered calls deliberately have no links into optimized bodies. + boolean opto = CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Opto.ordinal() && + CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Lower.ordinal(); + Type tmem = opto ? TypeMem.TOP : TypeMem.BOT; + Type ret = opto ? Type.TOP : tfp.ret(); + // Use the same widening before and after linking a callee. + return TypeTuple.make(Type.CONTROL,tmem,widenResult(ret,bound)); + } + + // A linked function for every concrete function + TypeTuple state = hasExtern + ? TypeTuple.make(Type.CONTROL,call.exposed(this),tfp.ret()) + : TypeTuple.STATE.dual(); + for( int i=1; i<nIns(); i++ ) + state = (TypeTuple)state.meet(in(i)._type); + // A reachable call whose presently linked exits are all unreachable + // keeps a conservative continuation. In particular, do not let a + // transient no-return result tear down the caller before/during SCCP. + // If the call remains out-of-line, its continuation can conservatively + // contribute class-init dependencies even though it never executes. + if( state.ctl()==Type.XCONTROL ) + return TypeTuple.make(Type.CONTROL,TypeMem.TOP,Type.TOP); + // Join before widening: FunPtrNode follows the live Return type, so + // joining it afterward would undo the widening. + return state.makeFrom(2,widenResult(state.ret().join(tfp.ret()),bound)); + } + + private static Type widenResult(Type ret, Type bound) { + // Recursion is a loop in disguise. For f(x-1)+1, the result + // flows Return -> CallEnd -> Add -> Phi -> Return, without + // crossing ParmNode.compute's widening point. Widen here too, + // using the loop rule in PhiNode.compute. Keep constants and + // above-center ranges narrow. + if( ret instanceof TypeInteger ti && !ti.isHigh() && !ti.isConstant() ) + ret = ti.same_but_slightly_wider_than().join(bound); + return ret; + } + + @Override + public Node idealize() { + return null; + } + + public byte maybeInline() { + byte inline = inlineCandidate(); + if( CodeGen.CODE._midAssert ) + return inline; + _inline = inline; + if( _inline > 0 ) + doInline(); + + return _inline; + } + + // Check if candidate right now, sets no flags + private byte inlineCandidate() { + if( _inline < -2 ) return -2; // Never + if( _folding ) return -2; // No, and will collapse anyways + if( !(in(0) instanceof CallNode call) ) return -2; // malformed because mid-death + + if( nIns()!=2 ) return -1; // Not exactly 1 link, but can change + + // Have exactly 1 linked function + ReturnNode ret = (ReturnNode)in(1); + FunNode fun = ret.fun(); + + // Preserve initializer boundaries until post-Opto cycle validation. + if( fun.isClz() ) return -2; + + // Heuristic forced inlining turned off via name + if( fun._name != null && + fun._name.endsWith("_noInline") ) + return -2; // Never, stop asking + + // Disallow self-recursive inlining (loop unrolling by another name). + for( int i=1; i < fun.nIns(); i++ ) + if( fun.cfg(i).fun()==fun ) // Check for linked call input inside "fun" + { addDep(fun); return -1; } // No, but can lose the recursive edge + + // Things that might improve, but not now a candidate + Node fptr = call.fptr(); + // Constant Function is being called, and its not-null + if( !(fptr._type instanceof TypeFunPtr tfp && tfp.notNull() && tfp.isConstant()) ) + { addDep(fptr); return -1; } + + // Arguments are correct + if( call.nargs()!=tfp.nargs() || !goodArgs(call,tfp) ) + { addDep(fptr); return -1; } + + // Encouraged inlining because small size and constructor. + int maxSize = fun.inlineLimit(); + // A large parse can leave only a tiny initializer. Recognize that + // and await cleanup to shrink the body. + if( fun._approxUIDs >= maxSize ) { + if( !smallBody(fun) ) return -1; + } else if( fun.body().cardinality() >= maxSize ) // Expensive size check + return -1; // Try later (if cleanup makes the real body small enough) + + // Can be cloned but not trivial + if( fun.nIns() > 2 ) { addDep(fun); return 2; } + + // A trivial candidate right now + return 1; + } + + // Bounded, conservative recognition of straight-line initializer boilerplate. + // Walk uses from the entry and stop at Return, never following callers or + // constant inputs. This also avoids scanning sparse MemMerge input arrays. + // Watch only this small prefix, including an unsupported node that may fold. + private static final Node[] TINYBODY = new Node[15]; // Tiny worklist + private boolean smallBody(FunNode fun) { + ReturnNode ret = fun.ret(); + for( Node in : ret._inputs ) + if( in!=null ) addDep(in); + if( ret.ctrl()!=fun ) return false; + TINYBODY[0] = fun; + int len=1; + for( int i=0; i<len; i++ ) { + Node n = TINYBODY[i]; + addDep(n); + if( n==ret ) continue; + if( n instanceof ParmNode parm && parm.region()!=fun ) return false; + if( n!=fun && !(n instanceof ParmNode) && + !(n instanceof StoreNode) && !(n instanceof MemMergeNode) ) + return false; // Something unexpected + if( n.nOuts()>TINYBODY.length ) return false; // Too big + for( Node use : n._outputs ) { + if( use==null ) continue; + int j=0; + while( j<len && TINYBODY[j]!=use ) j++; + if( j<len ) continue; // Already in TINYBODY + if( len==TINYBODY.length ) { addDep(use); return false; } // Too big + TINYBODY[len++] = use; // Add to worklist + } + } + return true; + } + + public void doInline() { + assert _inline > 0; // Only called when possible to inline + + ReturnNode ret = (ReturnNode)in(1); + FunNode fun = ret.fun(); + CallNode call = call(); + assert fun.sig().isa(call.fptr()._type); + + // Clone first + if( _inline==2 ) { + assert !CodeGen.CODE._midAssert; // Triggered inlining + // Remove the existing function linkage + call.unlink_all(); + // Clone the function body + FunNode fun2 = fun.copyBody(); + // Call uses the unique new function + Node fptr2 = new FunPtrNode(fun2.sig(),CodeGen.CODE._start,fun2.ret()).peephole(); + call.setDef(call.nIns()-1,fptr2); + // Link to the new function + call.link(fun2); + fun = fun2; + } + + // Trivial inlining: call site calls a single function; single function + // is only called by this call site. + doTrivialInlining(fun); + } + + boolean goodArgs( CallNode call, TypeFunPtr tfp ) { + for( int i=0; i<tfp.nargs(); i++ ) { + Node arg = call.arg(i+2); + // The graph's function-pointer type can still contain open forward + // references here. Compare against its recursively upgraded form, + // so deep mutability requirements participate before inlining. + Type formal = tfp.arg(i).upgradeType(Parser.TYPES); + if( !arg._type.isa(formal) || !arg._type.accessISA(formal) ) + { addDep(arg); return false; } + } + return true; + } + + + // Do trivial inlining. Inlining does not need to clone code, merely + // triggers folding the Call/Fun and Return/CallEnd away. + private boolean doTrivialInlining( FunNode fun ) { + assert !CodeGen.CODE._midAssert; // Triggered inlining + // Trivial inline: rewrite + _folding = true; + // Rewrite Fun so the normal RegionNode ideal collapses + fun._folding = true; + fun.setDef(1,call().ctrl());// Bypass the Call; + fun.ret().setDef(3,null); // Return is folding also + + CodeGen.CODE.addAll(fun._outputs); + // Repeat defs 1 layer down, for users of Parm (Phis) + for( Node parm : fun._outputs ) + if( parm instanceof ParmNode ) + CodeGen.CODE.addAll(parm._outputs); + + // Inlining immediately blows all cache idepth fields past the inline point. + // Bump the global version number invalidating them en-masse. + CodeGen.CODE.invalidateIDepthCaches(); + // More peeps + CodeGen.CODE.add(fun); + CodeGen.CODE.add(this); + CodeGen.CODE.addAll(_outputs); + CodeGen.CODE.addAll(_inputs); + return true; // Always return true + } + + + @Override public Node pcopy(int idx, Node dep) { + return _folding ? in(1).in(idx) : null; + } + + @Override public Node copy() { + return _folding ? super.copy() : new CallEndNode(_rpc); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallNode.java new file mode 100644 index 00000000..f791225a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CallNode.java @@ -0,0 +1,220 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeRPC; +import com.seaofnodes.simple.type.XInt; +import com.seaofnodes.simple.util.Utils; +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + * 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; CodeGen.CODE._calls.add(this); } + public CallNode(CallNode call) { super(call); _loc = call._loc; } + + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + String fname = name(); + if( fname == null ) p.n(fptr()); + else p.p(fname); + p.p("( "); + for( int i=2; i<nIns()-1; i++ ) + p.n(in(i)).p(","); + return p.unchar(',').unchar(' ').close(); + } + public String name() { + if( fptr()._type instanceof TypeFunPtr tfp && tfp._isConstant() && tfp.notNull() ) { + FunNode fun = CodeGen.CODE._link(tfp); + if( fun !=null ) return fun._name; + if( fptr() instanceof ExternNode ex ) return ex._extern; + } + return null; + } + + + Node ctrl() { return in(0); } + Node mem () { return in(1); } + // Args mapped 1-to-1 on inputs, so conceptually start at 2 + public Node arg(int idx) { return in(idx); } + // Same arg accounting as TFPs, although the numbering starts at 2 + public 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(); } + // Error if not a TFP + public TypeFunPtr tfp() { return (TypeFunPtr)fptr()._type; } + + // Call is to an externally supplied code + public boolean external() { + if( !(fptr()._type instanceof TypeFunPtr tfp) ) return false; + if( XInt.isHigh(tfp.fidxs()) ) return true; + for( int fidx=XInt.next(tfp.fidxs(),-1); fidx>=0; fidx=XInt.next(tfp.fidxs(),fidx) ) + if( CodeGen.CODE.externFunc(fidx)!=null ) return true; + return false; + } + + // As in the escape chapter, native code may retain an argument even if + // this path never returns. StopCU reads this independently of CallEnd. + TypeMem exposed(Node dep) { + dep.addDepForwards(this); + dep.addDepForwards(fptr()); + if( _type!=Type.CONTROL || !external() ) return TypeMem.START; + Type input=dep.addDepForwards(mem())._type; + TypeMem escaped=input instanceof TypeMem m ? m : input.isHigh() ? TypeMem.TOP : TypeMem.BOT; + for( int i=2; i<nIns()-1; i++ ) escaped=escaped.escapes(dep.addDepForwards(arg(i))._type); + return TypeMem.make(1,Type.BOTTOM,false,false,false,escaped._escFs,escaped._escAs); + } + + + // 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() { + // Lowering may introduce calls after the call graph has been analyzed. + // These use the callee interface without reopening its parameter flow. + if( CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Lower.ordinal() ) return null; + 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. + Node progress = null; + if( fptr()._type instanceof TypeFunPtr tfp && tfp.nargs() == nargs() ) { + // If fidxs is negative, then infinite unknown functions + int[] fidxs = tfp.fidxs(); + if( !XInt.isHigh(fidxs) ) { + // Walk the bits and link + for( int fidx = XInt.next(fidxs,0); fidx > 0; fidx = XInt.next(fidxs,fidx) ) { + FunNode fun = CodeGen.CODE.link(fidx); + if( fun!=null && !fun._folding && !linked(fun) ) + progress = link(fun); + } + } + } + + return progress; + } + + // True if Fun is linked to this Call + public boolean linked( FunNode fun ) { + for( Node n : fun._inputs ) + if( n == this ) + return true; + return false; + } + + // Link so this calls fun + public Node link( FunNode fun ) { + assert !linked(fun); + CodeGen.CODE.add(fun).addDef(this); + for( Node use : fun._outputs ) + if( use instanceof ParmNode parm ) { + Node def = parm._idx==0 ? ConstantNode.seed(cend()._rpc).peephole() : arg(parm._idx); + CodeGen.CODE.add(parm).addDef(def); + // Linking can happen during post-SCCP inlining. Keep an RPC + // Parm's cached finite set synchronized with its newly added + // input instead of waiting for the next worklist visit. + if( parm._idx==0 && parm._type instanceof TypeRPC ) + parm._type = parm._type.meet(def._type); + } + // Call end points to function return + CodeGen.CODE.add(cend()).addDef(fun.ret()); + return this; + } + + // Unlink a single function + public void unlink( FunNode fun, int path ) { + assert linked(fun); + for( Node use : fun._outputs ) + if( use instanceof ParmNode ) + use.delDef(path); + fun.delDef(path); + CodeGen.CODE.add(fun); + CallEndNode cend = cend(); + ReturnNode ret = fun.ret(); + if( cend != null ) { + ret.keep(); + cend.delDef(cend._inputs.find(ret)); + CodeGen.CODE.add(cend); + ret.unkeep(); + } + assert !linked(fun); + } + + + // Unlink all linked functions + public void unlink_all() { + for( int i=0; i<_outputs._len; i++ ) + if( out(i) instanceof FunNode fun ) { + int path = fun._inputs.find(this); + unlink(fun,path); + i--; + } + } + + @Override + public Parser.ParseException err() { + if( fptr()._type == Type.BOTTOM ) + return null; // Wrong, but failing earlier + if( !(fptr()._type instanceof TypeFunPtr tfp) ) + return Parser.error("Expected a function, but found " + fptr()._type, _loc); + 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)) || !arg(i+2)._type.accessISA(tfp.arg(i)) ) { + // Constructors return private memory and carry hidden self and + // selfMem arguments ahead of the source-level arguments. + int argno = i - (tfp.ret() instanceof TypeMem ? 2 : 0); + return Parser.error( "Argument #" + argno + " isa " + arg(i+2)._type + ", but must be a " + tfp.arg(i), _loc ); + } + + if( tfp.isHigh() ) + throw Utils.TODO(); // Infinite unknown TFPs? Should be fairly precise CG + + return null; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CalleeSaveNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CalleeSaveNode.java new file mode 100644 index 00000000..0afc5827 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CalleeSaveNode.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.type.Type; + +public class CalleeSaveNode extends ProjNode implements MachNode { + final RegMask _mask; + public CalleeSaveNode(FunNode fun, int reg, String label) { + super(fun,reg,label); + fun._outputs.pop(); + int i=0; + while( fun.out(i) instanceof PhiNode ) i++; + fun._outputs.insert(this,i); + _mask = new RegMask(reg); + } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return _mask; } + @Override public void encoding( Encoding enc ) { } + + @Override public Type compute() { return Type.BOTTOM; } + @Override public Node idealize() { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java new file mode 100644 index 00000000..0b8053e2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java @@ -0,0 +1,78 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.HashMap; + +/** + * A field offset. This becomes a constant *after* other optimizations which + * determine field sizes - some fields might be dead, or hold only zeros or + * small values. + */ + +public class ConFldOffNode extends ConstantNode { + public TypeStruct _ts; // Struct holding field + public final String _fname; // Field name + public ConFldOffNode( TypeStruct ts, String fname ) { super(TypeInteger.BOT); _ts = ts; _fname = fname; } + + + @Override public String label() { + return _fname == " len" ? "sizeof("+_ts._name+")" : "#"+_fname; + } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.p(label()); + } + + @Override public Node idealize() { + // Some array offsets are known early + if( _ts.isAry() ) { + // Length always at offset zero + if( _fname==" len" ) + return CodeGen.CODE.ZERO; + // The shift depends on the element + if( _fname=="<<" ) + return Node.con(_ts.aryScale()); + // The base depends on the layout (alignment for large elements may + // involve padding) and MAY depend on the length if I go for + // smaller array lengths. + } + return null; + } + + // Convert field offset to an integer + public Node asOffset() { + int fldx = _fname==" len" ? _ts._fields.length : _ts.find(_fname); + if( fldx == -1 ) return null; + // " len" selects the terminal size slot, not a Field. + if( fldx < _ts._fields.length && _ts._fields[fldx]._extern ) + return new ExternOffsetNode(_fname).peephole(); + return ConstantNode.make(TypeInteger.constant(_ts.offset(fldx))).peephole(); + } + + // Upgrade the internal type + @Override boolean _upgradeType( HashMap<String,Type> TYPES) { + boolean progress = super._upgradeType(TYPES); + TypeStruct ts = (TypeStruct)_ts.upgradeType(TYPES); + if( ts == _ts ) return progress; + _ts = ts; + return true; + } + + @Override public boolean eq(Node n) { + ConFldOffNode off = (ConFldOffNode)n; // Invariant + return _ts._name==off._ts._name && _fname.equals(off._fname) && super.eq(n); + } + @Override int hash() { return super.hash() * _ts._name.hashCode() * _fname.hashCode(); } + + @Override public Parser.ParseException err() { + return _ts._fref ? Parser.error("Unknown struct type '"+_ts._name+"'",null) : null; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConstantNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConstantNode.java new file mode 100644 index 00000000..eabf2639 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConstantNode.java @@ -0,0 +1,86 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + * A Constant node represents a constant value. + * <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 TypeNode { + public ConstantNode( Type type ) { + this(type,false); + } + protected ConstantNode( Type type, boolean raw ) { + super(type,new Node[]{CodeGen.CODE._start}); + assert raw || !(type instanceof TypeFunPtr tfp && tfp.isConstant()) : + "Unique function identities require a Return-linked FunPtrNode"; + _type = type; + } + public ConstantNode( ConstantNode con ) { super(con); } + + + // Semantic constant construction through Lower. A unique internal + // function identity carries a lifetime edge to the function's Return. + public static Node make( Type type ) { + if( type==Type. CONTROL ) return new CtrlNode(); + if( type==Type.XCONTROL ) return new XCtrlNode(); + if( CodeGen.CODE._phase != null && + CodeGen.CODE._phase.ordinal() > CodeGen.Phase.Lower.ordinal() ) + return raw(type); + if( type instanceof TypeFunPtr tfp && tfp.isConstant() ) { + FunNode fun = CodeGen.CODE._link(tfp); + if( fun != null ) + return new FunPtrNode(tfp,CodeGen.CODE._start,fun.ret()).init(); + // TODO: A singleton FIDX can also emerge after merging and + // guarding several function pointers. Preserve that identity + // even if its internal Return is no longer available. + return raw(type); + } + return new ConstantNode(type); + } + + // Exact construction for deserialization and graph-incomplete machine + // selection. No semantic classification, compute, idealize, or GVN. + public static ConstantNode raw( Type type ) { return new ConstantNode(type,true); } + public static ConstantNode raw( ConstantNode con ) { return new ConstantNode(con); } + + // Analysis input created from whole cloth, rather than a first-class + // runtime value. In particular, a constructor call descriptor does not + // own the constructor function; the class FunPtr does. + public static ConstantNode seed( Type type ) { return new ConstantNode(type,true); } + + @Override protected String repeatName() { + return _con==null || _con.toString().length()<=32 ? null : uniqueName(); + } + + @Override public String label() { return "Con"; } + @Override public Node copy() { return raw(this); } + + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + if( _con instanceof TypeFunPtr tfp && tfp._isConstant() && tfp.notNull() ) { + FunNode fun = CodeGen.CODE._link(tfp); + if( fun!=null && fun._name != null ) + return p.p("{ ").p(fun._name).p("}"); + } + return p.p(_con==null ? "---" : _con.toString()); + } + + @Override public boolean isConst() { return true; } + @Override public Type compute() { return _con; } + @Override public Node idealize() { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConvertNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConvertNode.java new file mode 100644 index 00000000..cdc890a7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ConvertNode.java @@ -0,0 +1,101 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; + +// Convert a value to an authoritative destination type. The destination is +// known structurally from a declaration; the source family might sharpen later. +public class ConvertNode extends Node { + private Type _dst; + + public ConvertNode(Type dst, Node val) { + super(null,val); + // Folding this conversion would replace the Return-linked FunPtrNode + // with an unhooked ConstantNode. + assert !(dst instanceof TypeFunPtr && val instanceof FunPtrNode); + _dst = dst; + } + + public Type dst() { return _dst; } + public Node val() { return in(1); } + + @Override public String label() { return "Convert_"+_dst.str(); } + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.p(label()).open().n(val()).close(); + } + + // The declaration fixes the result family during pessimistic parsing. + // From SCCP onward, preserve dead inputs and stronger facts in that family. + @Override public Type compute() { + Type src = val()._type; + if( CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Opto.ordinal() ) { + if( src.isHigh() ) return Type.TOP; + if( (src.isa(_dst) && src.accessISA(_dst)) ) return src; + } + return _dst; + } + + @Override public Node idealize() { + Type src = val()._type; + if( src==Type.BOTTOM || src==Type.TOP ) return null; + if( (src.isa(_dst) && src.accessISA(_dst)) ) return val(); + + // Managed arrays are represented by an object pointer, while C wants + // the address of the first element. The representation cast itself is + // zero-code; the Add skips the array header. + if( _dst==TypeInteger.BOT && src instanceof TypeMemPtr tmp && tmp._obj.isAry() ) + return new AddNode(new PtrToIntNode(val()).peephole(),Parser.off(tmp._obj,"[]")); + + // Integer (and nil-as-zero) to floating point. + if( (src instanceof TypeInteger || src==Type.NIL) && _dst instanceof TypeFloat ) + return new ToFloatNode(val()); + + // Convert to i64 first, then apply the destination's integer width. + if( src instanceof TypeFloat && _dst instanceof TypeInteger ) + return new ConvertNode(_dst,new ToIntegerNode(val()).peephole()); + + // Narrow integers produce the declared sign/zero extension. + if( src instanceof TypeInteger && _dst instanceof TypeInteger dst ) { + if( dst._min==0 ) + return new AndNode(null,val(),con(dst._max)); + int shift = Long.numberOfLeadingZeros(dst._max)-1; + Node shf = con(shift); + if( shf._type==TypeInteger.ZERO ) return val(); + return new SarNode(null,new ShlNode(null,val(),shf.keep()).peephole(),shf.unkeep()); + } + + // Narrow f64 to f32. + if( src instanceof TypeFloat && _dst instanceof TypeFloat ) + return new RoundF32Node(val()); + + // Wait for more source information, or report an error after Opto. + return null; + } + + @Override boolean _upgradeType(HashMap<String,Type> TYPES) { + Type dst = _dst.upgradeType(TYPES); + if( dst==_dst ) return false; + unlock(); + _dst = dst; + return true; + } + + @Override public Parser.ParseException err() { + if( val()._type==Type.BOTTOM || + val()._type instanceof TypeNil src && src.nullable() && _dst instanceof TypeNil dst && dst.notNull() ) { + Node out = out(0); + if( out instanceof PhiNode phi ) out = phi.out(0); + if( out instanceof StoreNode st ) + return Parser.error( "'" + ((TypeMemPtr)st.ptr()._type)._obj._name + "' is not fully initialized, field '" + st._name + "' is only partially set in the constructor", null ); + } + return Parser.error("Type "+val()._type.str()+" is not of declared type "+_dst.str(),null); + } + + @Override public boolean eq(Node n) { return _dst==((ConvertNode)n)._dst; } + @Override int hash() { return _dst.hashCode(); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/CtrlNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CtrlNode.java new file mode 100644 index 00000000..813e6fcb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/CtrlNode.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; + +public class CtrlNode extends CFGNode { + public CtrlNode() { super(CodeGen.CODE._start); } + + @Override public String label() { return "Ctrl"; } + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p("Cctrl"); } + @Override public boolean isConst() { return true; } + @Override public Type compute() { return Type.CONTROL; } + @Override public Node idealize() { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/DivNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/DivNode.java new file mode 100644 index 00000000..0db2b7a2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/DivNode.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.type.TypeFloat; +import java.util.BitSet; + +public class DivNode extends ArithNode { + @Override boolean allowFloat() { return true; } + public DivNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + public DivNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs, mode); } + + + @Override public String op() { return _mode==1 ? "//" : "/"; } + + @Override long doOp( long x, long y ) { return y==0 ? 0 : x / y; } + @Override double doOp(double x, double y ) { return x / y; } + @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + // Div of 1. + if( in(2)._type == TypeInteger.TRUE && in(1)._type.isa(compute()) ) + return in(1); + + // Div of constant + if( _mode==2 ) { + if( in(2)._type == TypeFloat.constant(1.) && in(1)._type.isa(compute()) ) + return in(1); + if( in(2)._type instanceof TypeFloat f && f.isConstant() ) + return new MulNode(in(1),ConstantNode.make(TypeFloat.constant(1.0/f.value())).peephole(),_mode); + } + + return super.idealize(); + } + + @Override Node copy(Node lhs, Node rhs) { return new DivNode(lhs,rhs,_mode); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/EscapeNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/EscapeNode.java new file mode 100644 index 00000000..f7518184 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/EscapeNode.java @@ -0,0 +1,109 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + * Merge public and private aliases. + * 0 - control + * 1 - private pointer + * 2 - private alias + * 3 - public alias + */ +public class EscapeNode extends TypeNode { + + public EscapeNode(Field fld, Node self, Node priv, Node pub ) { + super(fld,null,self,priv,pub); + } + public EscapeNode(EscapeNode esc ) { + super(esc); // Copy constructor + // Except we do not want/need self ptr past Opto. + // *NOTE* the weird direct stomp; this is correct because this + // constructor does NOT set the backedges, because it is used to + // copy-construct an entire graph and ALL edges will get rewritten. + _inputs.set(1,null); + _con = esc._con; + } + + // Pointer to some private (unescaped) memory from a NewNode + public Node self() { return in(1); } + public Node priv() { return in(2); } + public Node pub () { return in(3); } + public Field fld() { return (Field)_con; } + + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + p.p("Esc#").p(fld()._alias).p(" {"); + if( self()==null ) p.p("---"); + else p.n(self()).p(","); + p.n(priv()).p("}, "); + p.n(pub ()); + return p; + } + + @Override + public Type compute() { + if( priv()._type.isHigh() ) return TypeMem.TOP; + if( pub ()._type.isHigh() ) return TypeMem.TOP; + TypeMem mpriv = (TypeMem)priv()._type; + TypeMem mpub = (TypeMem)pub ()._type; + // Private memory comes from an allocation or other non-aliased source. + // Public memory comes from anywhere, might be bulk memory with a very weak mem type. + Field fld = fld(); + assert mpriv._alias==1 || mpriv._alias==fld._alias; + assert mpub ._alias==1 || mpub ._alias==fld._alias; + Type tpriv = mpriv._t instanceof TypeStruct ts && ts.findAlias(fld._alias)!=-1 + ? ts.at(ts.findAlias(fld._alias))._t + : mpriv._t; + assert !(mpub._t instanceof TypeStruct); // Never expect this, but if it starts to happen, need to optimize + Type tpub = mpub._t; + + // t is meet of pub & priv. Pub memory can be very weak (e.g. alias 1) + // and we're no worse than the declared field type + Type t = tpriv.meet(tpub).join(fld._t); + int[] fidxes = XInt.meet(mpriv._escFs, mpub._escFs); + int[] aliases = XInt.meet(mpriv._escAs, mpub._escAs); + return TypeMem.make(fld._alias, t, false, false, fld._final, fidxes, aliases).escapesFrom(t); + } + + @Override public Node idealize() { + // Sharpen private memory input + if( priv() instanceof MemMergeNode mmm ) { + setDef(2,mmm.alias(fld()._alias)); + return this; + } + // Sharpen public memory input + if( pub() instanceof MemMergeNode mmm ) { + setDef(3,mmm.alias(fld()._alias)); + return this; + } + // New allocation is dead + if( self()._type.isHigh() ) + if( pub()._type.isa(_type) ) return pub(); + else addDep(pub()); + return null; + } + +// Upgrade the internal type +@Override boolean _upgradeType( HashMap<String,Type> TYPES) { + Type t = _con.upgradeType(TYPES); + if( t == _con ) return false; + unlock(); // Unlock before changing _con + _con = t; + return true; +} + + @Override public boolean eq(Node n) { + if( fld()._alias!=((EscapeNode)n).fld()._alias ) + return false; + assert _con == ((EscapeNode)n)._con; + return true; + } + + @Override int hash() { return _con.hashCode(); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternNode.java new file mode 100644 index 00000000..2d1dc29f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternNode.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; + +import java.util.BitSet; +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + A constant with external linkage. + */ + +public class ExternNode extends ConstantNode { + public final String _extern; + // An exact external function identity has no local FunNode/Return to own. + // Its lifetime and eventual relocation are carried by the extern symbol + // and CodeGen's FIDX-to-extern mapping instead. + public ExternNode(Type t, String ex) { super(t,true); _extern = ex; } + + + @Override public String label() { return "#"+_con+":"+_extern; } + + @Override public boolean eq(Node n) { return this==n; } + + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java new file mode 100644 index 00000000..23ed5468 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.HashMap; +import java.util.IdentityHashMap; + +// A field address supplied by the C linker, not a displacement from the base. +// The class base remains available for ideal type/alias analysis. Instruction +// selection replaces it with this symbol's address and a zero displacement. +public class ExternOffsetNode extends ExternNode { + public ExternOffsetNode(String name) { super(TypeInteger.BOT,name); } + + @Override public Node copy() { return new ExternOffsetNode(_extern); } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/FRefNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FRefNode.java new file mode 100644 index 00000000..644f1434 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FRefNode.java @@ -0,0 +1,50 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Var; +import com.seaofnodes.simple.type.Type; + +/** + * 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. + + * FRefs can be external references, to other Simple defined values in ELF + * files, or external FFI/C libs. If external, a file system lookup needs to + * find it in another file, and the name (one time) change to match the found + * location. + * + * Example, a ambiguous name "foo" is looked up externally; it might resolve + * to "A.B.foo" or "libc.foo" or the like; the name is changed to match. + */ +public class FRefNode extends ConstantNode { + public static final Type FREF_TYPE = Type.BOTTOM; + public String _name; + public final Parser.Lexer _loc; // Source location + + public FRefNode( String name, Parser.Lexer loc ) { this(name,loc,FREF_TYPE); } + public FRefNode( String name, Parser.Lexer loc, Type type ) { + super(type); + _name = name; + _loc = loc; + } + + @Override public String label() { return "FRef"+_name; } + + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.p("FRef_").p(_name); + } + + @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 '"+_name+"'",_loc); } + + @Override public boolean eq(Node n) { return this==n; } + @Override int hash() { return _name.hashCode(); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/FldOffNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FldOffNode.java new file mode 100644 index 00000000..535241a6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FldOffNode.java @@ -0,0 +1,54 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.HashMap; + +/** + * A field offset. This becomes a constant *after* other optimizations which + * determine field sizes - some fields might be dead, or hold only zeros or + * small values. + */ + +public class FldOffNode extends TypeNode { + public final String _fname; // Field name + public final Parser.Lexer _loc; // + public FldOffNode( Node base, String fname, Parser.Lexer loc ) { super(TypeInteger.BOT,base); _fname = fname; _loc=loc; } + + + @Override public String label() { + return _fname == " len" ? "sizeof()" : "#"+_fname; + } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.p(label()); + } + + @Override public Type compute() { + return Type.BOTTOM; + } + + @Override public Node idealize() { + // When base() gets typed, this xforms into a ConFldOffNode + if( in(0)._type instanceof TypeMemPtr tmp ) + return new ConFldOffNode(tmp._obj,_fname); + return null; + } + + @Override public boolean eq(Node n) { + FldOffNode off = (FldOffNode)n; // Invariant + return _fname.equals(off._fname) && super.eq(n); + } + @Override int hash() { return super.hash() * _fname.hashCode(); } + + @Override public Parser.ParseException err() { + if( in(0)._type instanceof TypeMemPtr tmp && tmp._obj._fref ) + return Parser.error("Unknown struct type '"+tmp._obj._name+"'",null); + return null; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunNode.java new file mode 100644 index 00000000..ad79130e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -0,0 +1,463 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.Arrays; +import java.util.BitSet; +import java.util.HashMap; +import java.util.IdentityHashMap; +import static com.seaofnodes.simple.codegen.CodeGen.CODE; + +public class FunNode extends RegionNode { + + // When set true, this Call/CallEnd/Fun/Return is being trivially inlined + public boolean _folding; + public boolean folding() { return _folding; } + + private TypeFunPtr _sig; // Initial signature + private ReturnNode _ret; // Return pointer + + public String _name; // Debug name + + public int _approxUIDs; // Approximate function size, used as a inlining heuristic + public CompUnit _compunit; // Defined in source file + public final boolean _extern; // External function; no code, name only + + private FunNode( Parser.Lexer loc, Node[] nodes, TypeFunPtr sig, String name, CompUnit compunit, boolean ext ) { + super(loc,nodes); + _name = name; + _sig = sig; + _extern = ext; + _compunit = compunit; + } + public FunNode( Parser.Lexer loc, TypeFunPtr sig, String name, CompUnit compunit, Node... nodes ) { this(loc,nodes,sig,name,compunit,false); } + public FunNode( FunNode fun ) { + super( fun, fun._loc ); + _sig = fun.sig(); + _name = fun._name; + _compunit = fun._compunit; + _extern = fun._extern; + } + + + @Override public String label() { return _name == null ? "$fun"+_sig.fidx() : _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 parm(int idx) { + ParmNode pidx = null; + for( Node n : _outputs ) + if( n instanceof ParmNode parm && parm._idx==idx ) + { assert pidx==null; pidx=parm; } + return pidx; + } + public ParmNode rpc() { return parm(0); } + + // 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; } + public ReturnNode ret() { assert _ret!=null; return _ret; } + + // Signature can improve over time + public TypeFunPtr sig() { return _sig; } + public void setSig( TypeFunPtr sig ) { + assert sig.isa(_sig) || resolvesForwardSelf(sig,_sig); + if( _sig != sig ) { + CODE.add(this); + // Changing the signature can allow more inlining + if( _ret != null ) + for( Node use : ret().outs() ) + if( use instanceof CallEndNode || use instanceof FunPtrNode ) + CODE.add(use); + unlock(); + _sig = sig; + } + } + + // A class initializer starts parsing with an open singleton receiver and + // closes that same receiver after discovering its fields. Function + // arguments are contravariant, so ordinary isa does not describe this + // parser-time forward-reference resolution even though every other part + // of the signature sharpens normally. + private static boolean resolvesForwardSelf(TypeFunPtr sig, TypeFunPtr old) { + if( !(sig.arg(0) instanceof TypeMemPtr now) || + !(old.arg(0) instanceof TypeMemPtr prior) || + now._obj._name != prior._obj._name || + !prior._obj._open || now._obj._open ) + return false; + return sig.makeFrom(old.arg(0),0).isa(old); + } + + @Override boolean _upgradeType( HashMap<String,Type> TYPES) { + TypeFunPtr sig = (TypeFunPtr)_sig.upgradeType(TYPES); + if( sig == _sig ) return false; + unlock(); + _sig = sig; + return true; + } + + public void setName( String name ) { + if( _name==null ) + _name = _compunit==null || _compunit._cname==null || _compunit._cname.indexOf('.')==-1 + ? name + : (_compunit._cname+"."+name).intern(); + } + + @Override + public Type compute() { + // Only dead if no callers after SCCP + if( unknownCallers() ) + return Type.CONTROL; + Type t = Type.XCONTROL; + for( int i = 1; i < nIns(); i++) { + // Since there are no unknown callers, the Start input is only a + // fake hook and will be a Tuple with XControl, so ignore it. Need + // to be called from someplace other than Start. + if( !(in(i) instanceof StartNode) ) + t = t.meet(in(i)._type); + } + return t; + } + + @Override + public Node idealize() { + + // Some linked path dies, except unknown caller hooks never kill Start. + Node progress = deadPath(unknownCallers()); + if( progress!=null ) { + if( nIns()==2 && in(1) instanceof CallNode call ) + CODE.add(call.cend()); // If no Start and one call, check for inline + return progress; + } + + // Upgrade inferred or user-written return type to actual + if( _ret!=null && _ret._type instanceof TypeTuple tt && + tt.ret() != _sig.ret() && tt.ret().isa(_sig.ret()) ) { + setSig(_sig.makeFrom(tt.ret())); + return this; + } + + // Attempt to get rid of the unknown caller. + // - Must be past pessimistic Iter, which discovers and links calls + // through class-field loads and other initially imprecise values. + // - FIDX is not in the escape set + // - Not called anywhere else; post Opto called-not-escaped functions need to still be hooked to start + // Opaque CLInit obligations need their bodies even without Call edges. + if( !isClz() && in(1) instanceof StartCUNode start ) { + boolean preOpto = CodeGen.CODE._phase == null || + CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal(); + if( !preOpto && !start.escapedFIDX(_sig.fidx()) && nIns()<=2 && _ret!=null ) { + _compunit._stop.delDef(_compunit._stop._inputs.find(ret())); + return removeDeadPath(1); + } else { + addDep(start); + } + } + + + // If no default/unknown caller, use the normal RegionNode ideal rules + // to collapse + if( unknownCallers() ) return null; + + // If folding (already inlined) down to a single input, become that input. + // Cannot collapse if not-inlining, and might not inline if call site + // calls multiple targets. + if( _folding && 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, + // unless folding + @Override public int idepth() { return _folding ? super.idepth() : 1; } + + // Bypass Region idom, always assume idom is Start + @Override public CFGNode idom(Node dep) { + return _folding ? cfg(1) : null; + } + + // Always in-progress until we run out of unknown callers + public boolean unknownCallers() { return nIns()>=2 && in(1) instanceof StartNode; } + + public boolean isModInit( ) { + // The one top-level <clinit> with no internal dots: + // "sys.<clinit>" is a module, but "sys.io.<clinit>" is not. + return isClz() && _name.indexOf('.')==_name.lastIndexOf('.'); + } + public boolean isClz ( ) { return isClz (_name); } + public boolean isInit( ) { return isInit(_name); } + public boolean isInstance() { return isInstance(_name); } + public static boolean isClz (String name ) { return name!=null && name.endsWith(".<clinit>"); } + public static boolean isInit(String name ) { return name!=null && name.endsWith("init>"); } + public static boolean isInstance(String name ) { return name!=null && name.endsWith(".<init>"); } + + // Allocation factories and instance initializers carry extra memory boilerplate. + public int inlineLimit() { + return isInstance() || (_name!=null && _name.endsWith(".<new>")) ? 200 : 100; + } + + // Function is public (callable from Start directly). + public boolean isPublic( ) { + // Never true for anonymous functions + if( _name == null ) return false; + // Only public for <clinit> and <init>, all others + // are found via field loads. The <init> Start hook also supplies its + // conservative pre-Opto construction inputs; StopCU separately keeps + // its private-memory return from escaping as a public result. + if( !isInit() ) return false; + // Private class, has to be found via program text, not linker + if( _name.charAt(0)=='_' ) return false; + + // No instances of "...clz1._clz2..." where the class name path + // includes a private class. + if( _name.indexOf("._") != -1 ) return false; + + // Public class <cl/init> + return true; + } + + @Override public boolean inProgress() { + return unknownCallers() && CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal(); + } + + // Build the function body set + public BitSet body() { + // Iter drains ordinary peeps between inlines; body discovery precedes + // this inline's folding, so function boundaries are stable. + assert !_folding; + // Forward control includes paths that never reach Return. + BitSet cfgs = new BitSet(); + walkCFG(this,cfgs); + cfgs.set(ret()._nid); + + // 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(); + // The function owns its Return even when it has no executable exit. + body.set(ret()._nid); + BitSet visit = new BitSet(); + int old; + do { + old = body.cardinality(); + visit.clear(); + walkDown(this, cfgs, body, visit); + } while( old != body.cardinality() ); + return body; + } + + private static void walkCFG(CFGNode n, BitSet cfgs) { + if( cfgs.get(n._nid) ) return; + cfgs.set(n._nid); + if( n instanceof ReturnNode ) return; + for( Node use : n._outputs ) { + if( !(use instanceof CFGNode cfg) || cfg instanceof FunNode ) continue; + if( cfg instanceof RegionNode ? cfg._inputs.find(n)>0 : cfg.in(0)==n ) + walkCFG(cfg,cfgs); + } + } + + private static boolean walkDown( Node n, BitSet cfgs, BitSet body, BitSet visit ) { + if( n==null ) return false; + if( visit.get(n._nid) ) return body.get(n._nid); + visit.set(n._nid); + // Visit self as CFG outside the function + if( n instanceof CFGNode && !cfgs.get(n._nid) ) + return false; + // Pretend the NewNode is a CFG, so its projections stay with it in loops + if( n instanceof NewNode nnn ) cfgs.set(n._nid); + // Visit n.cfg() outside of function + if( n.in(0)!=null && !cfgs.get(n.in(0)._nid) ) + return false; + // Phis inside the function must have their body flag set BEFORE + // recursion walks around a loop and finds them again. + if( n instanceof PhiNode phi && cfgs.get(phi.in(0)._nid) ) + body.set(phi._nid); // Find data cycles + boolean in = n.in(0)!=null || n instanceof CFGNode; + for( Node use : n._outputs ) { + // Will hit a backedge, so 'n' MUST be in the function, and must be + // set before the cyclic walk finds 'n' again. + if( use instanceof PhiNode puse && puse.in(0) instanceof LoopNode loop && puse.in(2)==n ) + body.set(n._nid); // Set before walk + in |= walkDown(use,cfgs,body,visit); + } + if( in ) body.set(n._nid); + return in; + } + + // Clone function body. Give function a new FIDX. + FunNode copyBody() { + // Build the function body BitSet + BitSet body = body(); + assert body.cardinality() < inlineLimit(); + // Walk the body, cloning + IdentityHashMap<Node,Node> map = new IdentityHashMap<>(); + BitSet visit = CodeGen.CODE.visit(); + bodyCopy( visit, body, map, this ); + bodyCopy( visit, body, map, _ret ); // May be disconnected from executable control. + visit.clear(); + bodyEdge( visit, body, map, this ); + bodyEdge( visit, body, map, _ret ); + visit.clear(); + assert map.size()==body.cardinality(); + + // New function/return cross-link each other + FunNode fun2 = (FunNode )map.get(this); + ReturnNode ret2 = (ReturnNode)map.get(_ret); + fun2._ret = ret2; + ret2._fun = fun2; + BitSet body2 = new BitSet(); + for( Node n : map.values() ) + body2.set(n._nid); + + // Remove non-body callers. Body-local call edges were cloned along + // with their CallEnds and must stay linked so their cloned types remain + // monotonic during the post-Opto Iter. + for( int i=1; i<fun2.nIns(); i++ ) + if( !(fun2.in(i) instanceof CallNode call) || !body2.get(call._nid) ) + fun2.removeDeadPath(i--); + + // Linking adds graph edges and worklist entries; identity-map order + // would make inlining depend on the host JVM's object hashes. + Node[] olds = map.keySet().toArray(new Node[0]); + Arrays.sort(olds, (x,y) -> Integer.compare(x._nid,y._nid)); + for( Node old : olds ) + if( old instanceof CallNode oldCall ) + relinkClonedCall(oldCall,(CallNode)map.get(oldCall),map); + + // Flip to a new FIDX to avoid confusion with the old one. + // This is a non-monotonic (sideways) type move, only applicable + // because fun2 is new. The fidx is also a purely local fiction, + // since this body will inline and the fidx goes dead. + fun2._sig = _sig.makeFrom(CodeGen.CODE._fidxs.nextInline()); + return fun2; + } + + private static void relinkClonedCall( CallNode oldCall, CallNode call, IdentityHashMap<Node,Node> map ) { + CallEndNode oldCend = oldCall.cend(); + if( oldCend==null || oldCend._folding ) + return; + for( int i=1; i<oldCend.nIns(); i++ ) { + ReturnNode oldRet = (ReturnNode)oldCend.in(i); + FunNode oldFun = oldRet.fun(); + FunNode fun = (FunNode)map.get(oldFun); + if( fun==null ) fun = oldFun; + if( !call.linked(fun) ) + call.link(fun); + } + CallEndNode cend = call.cend(); + if( cend.nIns() > 1 ) + cend._type = cend.compute(); + } + + // Clone small function + private void bodyCopy( BitSet visit, BitSet body, IdentityHashMap<Node,Node> map, Node n ) { + if( n==null ) return; + if( !body.get(n._nid) ) return; // Not part of the body + if( visit.get(n._nid) ) return; // Been there, done that + visit.set(n._nid); + Node m = n.copy(); + CodeGen.CODE.add(m); + map.put(n,m); + m._type = n._type; + for( Node x : n._outputs ) + bodyCopy(visit,body,map,x); + } + private void bodyEdge( BitSet visit, BitSet body, IdentityHashMap<Node,Node> map, Node n ) { + if( n==null ) return; + if( !body.get(n._nid) ) return; // Not part of the body + if( visit.get(n._nid) ) return; // Been there, done that + visit.set(n._nid); + Node m = map.get(n); + for( Node e : n._inputs ) { + Node x = e; + if( e != null ) { + if( n instanceof CallEndNode cend && e != cend.call() && + (!(e instanceof ReturnNode ret) || map.get(ret.fun()) == null) ) + continue; + x = map.get(e); + if( x == null ) { + if( n instanceof CallEndNode cend && e != cend.call() ) + continue; + x = e; + } + } + m.addDef(x); + } + for( Node x : n._outputs ) + bodyEdge(visit,body,map,x); + } + + + // no 2 FunNodes are ever GVN'able + @Override public boolean eq( Node n ) { + return this==n; + } + + @Override public int hash() { + return _sig.hashCode(); + } + + // ------------ + // MachNode specifics, shared across all CPUs + + // Function may call other functions? X86 requires 16b aligned SP on + // outbound calls, but not leaf. Set during an unrelated RA scan. + public boolean _hasCalls; + + // Maximum incoming argument slot, based on function type + public int _maxArgSlot; + + // Frame adjust, including padding, set during early Encoding + public int _frameAdjust; + + public String op() { return _frameAdjust==0 ? "entry" : "subi"; } + public void postSelect(CodeGen code) { + code.link(this); + // Max slot seen past space reserved for incoming arguments + _maxArgSlot = code._mach.maxArgSlot(sig()); + } + public RegMask regmap(int i) { return null; } + public RegMask outregmap() { return null; } + public void encoding( Encoding enc ) { throw Utils.TODO(); } + public void asm(CodeGen code, SB sb) { + if( _frameAdjust!=0 ) + sb.p("rsp -= #").p(_frameAdjust); + } + + public void computeFrameAdjust(CodeGen code, int maxReg) { + // Max slot seen, or 0. + int maxSlot = Math.max(maxReg - code._mach.regs().length,_maxArgSlot); + // Frame adjust before rounding, can be negative for no spills + int size = maxSlot - _maxArgSlot; + _frameAdjust = size*8; + } + + // Given a high register number and during encoding compute the stack + // pointer offset. Pre-RA we lack the fun._frameAdjust. We also need a + // FunNode. + public int computeStackOffset(CodeGen code, int reg) { + String[] regs = code._mach.regs(); + int stackSlot = reg - regs.length; + int slotRotate = stackSlot < _maxArgSlot + ? stackSlot + (_frameAdjust>>3) + : stackSlot - _maxArgSlot; + return slotRotate*8; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunPtrNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunPtrNode.java new file mode 100644 index 00000000..3d6031b8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/FunPtrNode.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// Constant function pointer whose type follows the callee signature. +public class FunPtrNode extends TypeNode { + public FunPtrNode(TypeFunPtr tfp, StartNode start, ReturnNode ret) { + super(tfp,start,ret); + } + public FunPtrNode(FunPtrNode fptr) { super(fptr); } + + + public ReturnNode ret() { return (ReturnNode)in(1); } + public FunNode fun() { return ret().fun(); } + + @Override public String label() { return "#"+_con; } + @Override public Node copy() { + FunPtrNode fptr = new FunPtrNode((TypeFunPtr)_con,null,null); + fptr._type = _type; + return fptr; + } + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + FunNode fun = CodeGen.CODE._link(((TypeFunPtr)_con).fidx()); + return fun!=null && fun._name!=null + ? p.p("{ ").p(fun._name).p("}") + : p.p(_con); + } + + @Override public boolean isConst() { return true; } + @Override public Type compute() { + ReturnNode ret = ret(); + if( ret == null ) return _con; + if( !(ret._type instanceof TypeTuple tt) ) return ret._type.oob(); + TypeFunPtr tfp = ret.fun().sig(); + // Both the sig and the return value must be true, so JOIN both + Type tret = tfp._ret.join(tt.ret()); + return tfp.makeFrom(tret); + } + @Override public Node idealize() { + ReturnNode ret = ret(); + if( ret==null ) return null; + // Function died (never executed), but fcn ptr is alive. + // Can be checked for null, or for unequals another FunPtr. + FunNode fun = addDep(ret.fun()); + TypeFunPtr sig = fun.sig(); + if( sig != _con ) + { liftType(sig); return this; } + // TODO: If the function body dies but this value remains live only as + // a null/equality sentinel, preserve the identity without the Return. + if( fun.isDead() ) + { setDef(1,null); liftType(sig.makeFrom(Type.TOP)); return this; } + return null; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardMach.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardMach.java new file mode 100644 index 00000000..202b1ab8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardMach.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; + +public class GuardMach extends GuardNode implements MachNode { + public GuardMach(GuardNode guard) { super(guard); } + @Override public String op() { return label(); } + @Override public RegMask regmap(int i) { assert i==1; return RegMask.FULL; } + @Override public RegMask outregmap() { return RegMask.FULL; } + @Override public int twoAddress() { return in(1)._type==Type.BOTTOM ? 0 : 1; } + @Override public void encoding(Encoding enc) { } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(in(1))); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardNode.java new file mode 100644 index 00000000..e985da72 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/GuardNode.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeScalar; +import com.seaofnodes.simple.util.Utils; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// A branch-proven refinement to zero/null or non-zero/non-null. The value +// family is deliberately taken from the input when it becomes known. +public class GuardNode extends Node { + public final boolean _nonZero; + + public GuardNode(boolean nonZero, Node ctrl, Node val) { + super(ctrl,val); + _nonZero = nonZero; + } + + public GuardNode(GuardNode guard) { + super(guard); + _nonZero = guard._nonZero; + } + + + @Override public String label() { return _nonZero ? "(!=0)" : "(==0)"; } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.p(label()).n(in(1)); + } + + @Override public Type compute() { + // Guard is a pinned, path-local fact. If its control path is not + // executable, the fact is not executable either, regardless of how the + // value input sharpens later. + Type c = in(0)._type; + if( c != Type.CONTROL && c != Type.BOTTOM ) + return Type.TOP; + Type t = in(1)._type; + if( t==Type.BOTTOM || t==Type.TOP ) return t; + // The scalar envelope does not identify a value family. Preserve it + // until SCCP learns whether this is an int, float, pointer, or fptr; + // choosing generic ptr here can be weaker than a later concrete type. + if( t==TypeScalar.BOT || t==TypeScalar.TOP ) return t; + Type t2 = _nonZero ? t.nonZero() : t.makeZero(); + return t2==null ? (_type==null ? Type.TOP : _type) : t2; // No sane answer for e.g. not-zero of a zero + } + + @Override public Node idealize() { + if( in(0)._type != Type.CONTROL && in(0)._type != Type.BOTTOM ) + return null; + if( in(1)._type==Type.BOTTOM || in(1)._type==Type.TOP ) return null; + if( _type.isHighOrConst() && in(1)._type.isa(_type) ) + return in(1); + return null; + } + + @Override public boolean eq(Node n) { return _nonZero==((GuardNode)n)._nonZero; } + @Override int hash() { return _nonZero ? 1 : 0; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/IfNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/IfNode.java new file mode 100644 index 00000000..471a28fe --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/IfNode.java @@ -0,0 +1,100 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +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 + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + p.p("if( "); + if( in(1)==null ) p.p("never"); + else p.n(in(1)); + return p.p(" )"); + } + + 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 + assert !_inputs.isEmpty(); + 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== XInt.EMPTY) ) ? 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( addDep(dom) instanceof IfNode iff && + prior instanceof CProjNode prj && + (addDep(iff.pred())==pred() || + pred() instanceof GuardNode cast && cast._nonZero && iff.pred()==cast.in(1) ) ) { + setDef(1,con( prj._idx==0 ? 1 : 0 )); + return this; + } + return null; + } + + // MachNode variants need to support this and negate the conditional test. + // The following CProjs will be inverted by the caller. + public void negate() { throw Utils.TODO(); } + + // Negate the sense of a test + public static String negate( String bop ) { + return switch( bop ) { + case "<" -> ">="; + case "<=" -> ">" ; + case "==" -> "!="; + case "!=" -> "=="; + case ">" -> "<="; + case ">=" -> "<" ; + default -> throw Utils.TODO(); + }; + } + + // Swap compare operands + public static String swap( String bop ) { + return switch( bop ) { + case "<" -> ">" ; + case "<=" -> ">="; + case "==" -> "=="; + case "!=" -> "!="; + case ">" -> "<" ; + case ">=" -> "<="; + default -> throw Utils.TODO(); + }; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/LoadNode.java new file mode 100644 index 00000000..e1a495ad --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/LoadNode.java @@ -0,0 +1,331 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; +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, Node ctrl, Node mem, Node ptr, Node off) { + super(loc, name, alias, true, Type.BOTTOM, ctrl, mem, ptr, off); + } + + + @Override boolean canDrop(MemOpNode other, Node dep) { + return super.canDrop(other,dep) && !clobbered(dep); + } + + // Check only immediate memory users. Writers clobber memory; Phis and + // aggregates might lead to a clobber, so stop rather than search further. + private boolean clobbered(Node dep) { + Node mem = mem(); + dep.addDepForwards(mem); + for( Node use : mem._outputs ) { + if( use==null ) continue; + dep.addDepForwards(use); + if( use instanceof StoreNode || use instanceof PhiNode || + use instanceof MemMergeNode || + use instanceof NewNode || use instanceof EscapeNode || use instanceof CallNode || + use instanceof CLInitNode ) return true; + } + return false; + } + + // Debugger label + @Override public String label() { return "ld_"+mlabel(); } + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p(".").p(_name); } + + @Override + public Type compute() { + Type mem0 = mem()._type; + Type ptr0 = ptr()._type; + // Validate argument types + if( ptr0.isHigh() ) + return TypeScalar.TOP; + if( !(mem0 instanceof TypeMem mem) ) + return mem0.isHigh() ? TypeScalar.TOP : TypeScalar.BOT; + if( mem._t == Type.TOP ) + return TypeScalar.TOP; + if( ptr0 == Type.NIL ) + return Type.BOTTOM; // Load from nil is error + if( ptr0 == Type.BOTTOM ) + return Type.BOTTOM; // Error in is error out + if( !(ptr0 instanceof TypeMemPtr tmp) ) + return scalar(mem._t); + if( tmp.nullable() ) + return Type.BOTTOM; // Load from nil-able is error + + // Load field from object + Field pfld = tmp._obj.field(_name); + // No field? Open objects might yet get the field when falling; + // closed objects with missing field are an error. + if( pfld == null ) + return tmp.isHigh() || tmp._obj.isHigh() ? TypeScalar.TOP : TypeScalar.BOT; + + Type t = pfld._t; + boolean readonly = t instanceof TypeMemPtr view && view._ro; + // Load member of constant array + if( t instanceof TypeConAry ary ) + t = ary.elem(); // TODO: if offset is known, can peek the constant + if( mem._alias==_alias ) + t = t.join(mem._t); + + // A deeply read-only base produces a deeply read-only value. The + // generic declared field type must not cast this information away. + if( ptr0.isFinal() || readonly ) + t = t.makeRO(); + return scalar(t); + } + + // Loads produce ordinary scalar values. Field/memory joins can still + // expose the enclosing global lattice bounds through an uninitialized + // Type.TOP/BOTTOM field; keep those results inside the scalar envelope. + private static Type scalar(Type t) { + return t==Type.TOP ? TypeScalar.TOP : t==Type.BOTTOM ? TypeScalar.BOT : t; + } + + @Override + public Node idealize() { + Node mem = mem(); + Node ptr = ptr(); + + // Null checks are handled by types. Array elements retain control for + // range checks, but reading an array's length has no index to check. + if( in(0)!=null && ptr._type instanceof TypeMemPtr tmp && + (!tmp._obj.isAry() || _name.equals("#")) ) { + setDef(0,null); + return this; + } + + // Forward-ref loads eventually sharpen to a declared type + Field fld; + if( ptr._type instanceof TypeMemPtr tmp && + (fld=tmp._obj.field(_name)) != null && + _alias != fld._alias) { + assert _alias==1 || _alias == fld._alias; + assert !tmp._obj._open && !tmp._obj._fref; + unlock(); // Alias participates in GVN semantics + _alias = fld._alias; + return this; + } + // Must sharpen alias first + if( _alias == 1 ) + return null; + + Node slice = CLInitNode.slice(mem,_alias,this); + if( slice!=mem ) { setDef(1,slice); return this; } + + if( mem instanceof MemMergeNode merge ) { + setDef(1,merge.alias(_alias)); + // In support of uplifting control to a prior dominating load, if + // we move our memory, recheck other loads to see if they can hoist. + for( Node use : in(1).outs() ) + if( use instanceof LoadNode ld && use != this && ptr == ld.ptr() ) + CodeGen.CODE.add(ld); + + return this; + } + + // 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().domLCA(ld.cfg0(),this) == ld.cfg0() && // Higher control means load is legal earlier + addDep(ld)._type.isa(_type) ) // and not rolling backwards + return ld; + + Node win = find(mem,ptr,null,null); + if( win!=null ) return folded(win); + if( ptr instanceof ReadOnlyNode ro ) ptr=ro.in(1); + + // 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 MemPhiNode memphi && memphi.region()._type == Type.CONTROL && memphi.nIns()== 3 && + // Retain control on both copies only if it dominates both paths. + // The Region itself, or a later range check, cannot move to its inputs. + (in(0)==null || cfg0().domLCA(memphi.region().idom(this),this)==cfg0()) && + // Offset can be hoisted + off() instanceof ConstantNode && + // Pointer can be hoisted + hoistPtr(ptr,memphi) ) { + + // Returning to this memory means no change only if the pointer + // is also loop invariant. A pointer Phi may select another object. + Node stop = memphi.region() instanceof LoopNode && + !(ptr instanceof PhiNode p && p.region()==memphi.region()) ? memphi : null; + if( find(memphi.in(2),ptr,stop,new BitSet())!=null || + (!(memphi.region() instanceof LoopNode) && find(memphi.in(1),ptr,null,new BitSet())!=null) ) { + // Integer loop Phis widen to i64. Keep the load's declared + // width at the result, as for ordinary typed loop variables. + if( _type instanceof TypeInteger && !declaredType().isa(_type) ) return null; + PhiNode phi = new PhiNode(_name,memphi.region()).keep(); + phi.setType(_type instanceof TypeInteger ? TypeInteger.BOT : _type); + for( int i=1; i<memphi.nIns(); i++ ) { + Node p = ptr instanceof PhiNode pp && pp.region()==memphi.region() ? pp.in(i) : ptr; + phi.addDef(load(memphi.in(i),p,stop,phi)); + } + CodeGen.CODE.add(phi.unkeep()); + return _type instanceof TypeInteger ? new ConvertNode(declaredType(),phi) : phi; + } + } + + return null; + } + + // Load a flavored zero from a New + private Node zero(NewNode nnn) { + Type zero = declaredType().makeZero(); + assert zero.isa(_type); // Catch an uninitialized non-null field + return castRO(new ConstantNode(zero).peephole()); + } + + // Search only; no new nodes or rewiring. Return a Store/New that folds, + // the unchanged loop memory, or a merge whose every arm folds. Null fails. + // Register dependencies so a later pointer/offset rewrite retries the query. + private Node find(Node mem, Node ptr, Node stop, BitSet visit) { + if( ptr instanceof ReadOnlyNode ro ) ptr=ro.in(1); + Field fld; + boolean external = ptr._type instanceof TypeMemPtr tmp && + (fld=tmp._obj.field(_name))!=null && fld._extern; + while( mem!=null ) { + addDep(mem); + if( mem==stop ) return mem; + switch( mem ) { + case MemMergeNode merge: mem=merge.alias(_alias); break; + case StoreNode st: + // Alias 1 is still unresolved, not a proven different field. + if( st._alias==1 ) return null; + addDep(st.ptr()); + if( _alias==st._alias && (ptr==st.ptr() || ptr==st.nnptr()) && off()==st.off() ) { + // A full-width load cannot narrow the forwarded value. + if( declaredType() instanceof TypeInteger ti && ti.log_size()==3 && + !addDep(st.val())._type.isa(_type) ) return null; + return st; + } + if( (external || _alias==st._alias) && (external || !neverAlias(ptr,st.ptr())) && + !addDep(off())._type.join(addDep(st.off())._type).isHigh() ) return null; + mem=st.mem(); + break; + case ProjNode proj: + // New's memory is private; public memory is joined at Escape. + if( external || !(proj.in(0) instanceof NewNode nnn) ) return null; + if( declaredType()==Type.BOTTOM ) return null; + return nnn; + case EscapeNode esc: + if( external ) return null; + addDep(esc.self()); + if( esc.self()==ptr ) { mem=esc.priv(); break; } + if( neverAlias(ptr,esc.self()) ) { mem=esc.pub(); break; } + return null; + case MemPhiNode phi: + addDep(phi.region()); + // Only the original loop closes a successful cycle. Another + // loop or an unfinished merge needs a separate proof. + if( visit==null || !(phi.region() instanceof RegionNode r) || r instanceof LoopNode || + r.inProgress() || phi.inProgress() || r._type!=Type.CONTROL ) return null; + if( visit.get(phi._nid) ) return phi; // Already proved every arm. + for( int i=1; i<phi.nIns(); i++ ) + if( find(phi.in(i),ptr,stop,visit)==null ) return null; + visit.set(phi._nid); + return phi; + default: return null; + } + } + return null; + } + + private Node folded(Node win) { + return win instanceof StoreNode st ? extend(castRO(st.val())) : zero((NewNode)win); + } + + // Materialize only after the profitability search succeeds. The value Phi + // stands for a load which comes back around the loop with unchanged memory. + private Node load(Node mem, Node ptr, Node stop, PhiNode value) { + Node win=find(mem,ptr,stop,new BitSet()); + if( win!=null ) { + if( win==stop ) return value; + if( win instanceof MemPhiNode mp ) { + PhiNode phi=new PhiNode(_name,mp.region()).keep(); + for( int i=1; i<mp.nIns(); i++ ) phi.addDef(load(mp.in(i),ptr,stop,value)); + return CodeGen.CODE.add(phi.unkeep().peephole()); + } + Node fold=folded(win); + if( fold!=null ) return fold.peephole(); + } + LoadNode ld=new LoadNode(_loc,_name,_alias,in(0),mem,ptr,off()); + return ld.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; + } + + // Read-Only is a deep property, and cannot be cast-away + private Node castRO(Node rez) { + if( (ptr()._type.isFinal() || declaredType().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( shift==0 ) { + if( !val._type.isa(_type) ) + { addDep(val); return null; } + return val; + } + Node shl = new ShlNode(null,val,shf.keep()).peephole(); + return new SarNode(null,shl,shf.unkeep()); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/LoopNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/LoopNode.java new file mode 100644 index 00000000..005a6624 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/LoopNode.java @@ -0,0 +1,106 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class LoopNode extends RegionNode { + public LoopNode( Parser.Lexer loc, Node xctrl, Node entry ) { super(loc,xctrl,entry,null); } + public LoopNode( LoopNode loop ) { super(loop); } + + + public CFGNode entry() { return cfg(1); } + public CFGNode back () { return cfg(2); } + + @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 validIDepth() ? _idepth : cacheIDepth(entry().idepth()+1); } + // 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 FunNode 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._ltree != _ltree && nested(_ltree,other._ltree) ) + return fun; // Found an exit, not an infinite loop + } + x = x.idom(); + } + // Found a no-exit loop. Insert an exit + NeverNode iff = (NeverNode)new NeverNode(1.2f,back()).peephole(); // Ideal never-branch + CProjNode t = new CProjNode(iff,0,"True" ).init(); + CProjNode f = new CProjNode(iff,1,"False").init(); + setDef(2,t); // True continues loop, False (never) exits loop + ReturnNode ret = fun.ret(); + iff._ltree = t._ltree = _ltree; + ret._ltree = f._ltree = fun._ltree; + + // Now fold control into the exit. Might have 1 valid exit, or an + // XCtrl or a bunch of prior NeverNode exits. + Node top = ConstantNode.make(Type.TOP).peephole(); + Node memout = new MemMergeNode(); + memout.addDef(f); // placeholder for control + memout.addDef(top); + for( Node u : _outputs ) + if( u instanceof PhiNode phi && phi._type.isa(TypeMem.BOT) ) + memout.addDef(phi); + memout.init(); + + Node ctrl = ret.ctrl(), mem = ret.mem(), expr = ret.expr(); + if( ctrl!=null && 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 + RegionNode r = new RegionNode(_loc,null,ctrl).init(); + ctrl = r; r._ltree = fun._ltree; + mem = new BulkMemPhiNode(r,mem).init(); + expr = new PhiNode(r,expr).init(); + } + // Append new Never exit + ctrl.keep().addDef(f ); + mem .keep().addDef(memout); + expr.keep().addDef(top); + } else { + ctrl = f .keep(); + mem = memout.keep(); + expr = top .keep(); + } + ret.setDef(0,ctrl.unkeep()); + ret.setDef(1,mem .unkeep()); + ret.setDef(2,expr.unkeep()); + // Force sane types to exit; might locally break monotonicity + ret.init(); + ret.out(0).init(); + stop.init(); + CodeGen.CODE.add(CodeGen.CODE._start); + CodeGen.CODE.add(mem); + + return fun; + } + + private static boolean nested(LoopTree inner, LoopTree outer) { + for( ; inner!=null; inner = inner._par ) + if( inner == outer ) + return true; + return false; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/MachConcreteNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MachConcreteNode.java new file mode 100644 index 00000000..b25fcaaf --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MachConcreteNode.java @@ -0,0 +1,26 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.type.Type; +import java.io.ByteArrayOutputStream; + +// 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 new AssertionError("Do not call compute on "+getClass().getSimpleName()); } + @Override public Node idealize() { throw new AssertionError("Do not call idealize on "+getClass().getSimpleName()); } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + p.open().p(op()).p(","); + for( int i=1; i<nIns(); i++ ) + (in(i)==null ? p.p(" ---") : p.n(in(i))).p(","); + return p.unchar(',').close(); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/MachNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MachNode.java new file mode 100644 index 00000000..025b5dc4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MachNode.java @@ -0,0 +1,60 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +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( CodeGen code ) { } + + // Register mask allowed on input i. 0 for no register. + RegMask regmap( int i ); + // Register mask allowed as a result. 0 for no register. + 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(); } + + // Set of killed registers + default RegMask killmap() { return null; } + + // 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; } + // Ok to switch arguments + default boolean commutes() { return false; } + + // Instructions cheaper to recreate than to spill, such as loading small constants + default boolean isClone() { return false; } + + // Make a clone of a cheap instruction + default Node copy() { return null; } + + // Encoding is appended, relocations gathered. + void encoding( Encoding enc ); + + // 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/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java new file mode 100644 index 00000000..cbf19c55 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java @@ -0,0 +1,154 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +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 { + + public MemMergeNode( Node ...nodes) { super(nodes); _type = TypeMem.BOT; } + public MemMergeNode(MemMergeNode mem) { super(mem); } + + + @Override public String label() { return "ALLMEM"; } + @Override public boolean isMem() { return true; } + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + p.p("MEM[ "); + for( int j=2; j<nIns(); j++ ) { + p.p(j); + p.p(":"); + Node n = in(j); + while( n instanceof ScopeNode loop ) { + p.p("Lazy_"); + n = loop.mem(); + } + if( n==null ) p.p("___ "); + else p.n(n).p(" "); + } + return p.unchar(' ').p("]"); + } + + + @Override public Type compute() { + Type tmem = in(1)._type; + if( !(tmem instanceof TypeMem defmem) ) + // A MemMerge is structurally memory even while its default input + // is a weak, not-yet-specialized Phi. + return tmem.isHigh() ? TypeMem.TOP : TypeMem.BOT; + // Is this a single private instance memory? + if( defmem._one ) { + if( !(defmem._t instanceof TypeStruct ts) ) + return defmem; + // Perfect singleton memory, so all updates are parallel and + // independent and stack. + for( int i=2; i<nIns(); i++ ) { + Node in = in(i); + if( in !=null && in._type.isHigh() ) + return TypeMem.TOP; + if( in instanceof StoreNode st ) { + Type val = ((TypeMem)st._type)._t; + Field old = ts.field(st._name); + // Lazy add an expected field for open structs. + if( old==null ) { assert ts._open; + old = Field.make(st._name,Type.BOTTOM,st._alias,st._init); + ts = ts.add(old); + } + Field fld = old.makeFrom(val); + // TODO: val leaks into perfect singleton memory, along with its fidxs and aliases + ts = ts.replace(fld); + } + } + return TypeMem.makePrivate(ts); + + } + + // Normal public memory; meet across escaped inputs + TypeMem mem = defmem; + for( int i=2; i<nIns(); i++ ) { + // Has this alias escaped? + if( XInt.bit(mem._escAs,i) && + in(i) != null && !in(i)._type.isHigh() ) { + mem = (TypeMem)mem.meet(in(i)._type); + } + } + return mem; + } + + @Override public Node idealize() { + assert nIns() != 0 && in(1)!=null; // Always have a default. if( nIns()==0 ) return null; + + // Fold defaults into the default + boolean progress=false, allDefault=true; + for( int i=2; i<nIns(); i++ ) { + if( in(1) == in(i) ) { setDef(i,null); progress=true; } + else { allDefault=false; } + } + + // If not merging any memory (all memory is just the default) + if( allDefault ) + return in(1); // Become default memory + + // Collapse stacked merged-mem + if( in(1) instanceof MemMergeNode mem ) { + // A dead memory cycle can lose its last use when we bypass mem. + // Keep the peephole's result alive until the worklist sees it. + keep(); + // Goal is to swap my default mem with mem's default mem + for( int i=2; i<mem.nIns(); i++ ) { + if( mem.in(i) != null ) { + // deeper default mem has a non-default + if( i>=nIns() || in(i)==null ) + setDefX(i,mem.in(i)); + } + } + CodeGen.CODE.add(mem.in(1)); + setDef(1,mem.in(1)); + return unkeep(); + } + + return progress ? this : null; + } + + + public Node in( Var v ) { return in(v._idx); } + + public Node alias( int alias ) { + assert !(in(1) instanceof BulkMemPhiNode bulk && bulk.isSplit(alias) && + (alias >= nIns() || in(alias)==null)); + return alias < nIns() && in(alias)!=null ? in(alias) : in(1); + } + + public Node alias( int alias, Node st ) { return setDefX(alias,st); } + + // Now one-time do a useless-phi removal + void _useless( ) { + for( int i=1; 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 public boolean eq( Node n ) { + return this==n; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemOpNode.java new file mode 100644 index 00000000..fa399e1d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemOpNode.java @@ -0,0 +1,140 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.lang.StringBuilder; +import java.util.*; + +/** + * Convenience common base for Load and Store. + * + * 0 - Control + * 1 - Memory + * 2 - Base of object, OOP + * 3 - Offset, integer types + * 4 - Value, for Stores only + */ +public abstract class MemOpNode extends TypeNode { + + // The equivalence alias class + public int _alias; + + // True if load-like, false if store-like. + // Used on CPU-specific combined memory+arithmetic ops. + // + // Stores produce memory (maybe as part of a tuple with other things), + // loads do not. + // + // Loads might pick up anti-dependencies on prior Stores, and never cause an + // anti-dependence themselves. + // + // Stores must maximally sink to the least dominator use. Loads can be + // opportunistically hoisted. + public final boolean _isLoad; + + // Field name. + public final String _name; + // Source location for late reported errors + public final Parser.Lexer _loc; + + public MemOpNode(Parser.Lexer loc, String name, int alias, boolean isLoad, Type decl, Node ctrl, Node mem, Node ptr, Node off) { + super(decl, ctrl, mem, ptr, off); + _name = name; + _alias = alias; + _loc = loc; + _isLoad = isLoad; + } + public MemOpNode(Parser.Lexer loc, String name, int alias, boolean isLoad, Type decl, Node ctrl, Node mem, Node ptr, Node off, Node value) { + this(loc, name, alias, isLoad, decl, ctrl, mem, ptr, off); + addDef(value); + } + public MemOpNode( Node ideal, MemOpNode mop ) { + super(ideal,mop instanceof LoadNode ld ? ld.declaredType() : mop._con); + _name = mop._name; + _alias = mop._alias; + _loc = mop._loc; + _isLoad= mop._isLoad; + } + + + static String mlabel(String name) { return "[]"==name ? "ary" : ("#"==name ? "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); } + + // Current declared field type. This is deliberately a live lookup: + // cyclic/forward types can sharpen by replacing the pointer's struct. + public Type declaredType() { + if( !(ptr()._type instanceof TypeMemPtr tmp) ) + return Type.BOTTOM; + Field fld = tmp._obj.field(_name); + return fld == null || fld._t == null ? Type.BOTTOM : fld._t; + } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return _printMach(p); } + protected ExprPrinter<Node> _printMach(ExprPrinter<Node> p) { throw Utils.TODO(); } + + + // Extra conditions for factoring matching memory operations through a Phi. + // The caller has already checked the opcode, input shape and control. + boolean canDrop(MemOpNode other, Node dep) { + dep.addDep(ptr()); + return _alias>1 && _alias==other._alias && declaredType()==other.declaredType() && err()==null; + } + + @Override + public boolean eq(Node n) { + MemOpNode mem = (MemOpNode)n; // Invariant + return _alias==mem._alias && super.eq(mem); + } + + @Override + int hash() { return _alias ^ super.hash(); } + + @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 Parser.error( "Might be null accessing '" + _name + "'",_loc); + if( tmp._obj.field(_name)==null && + CodeGen.CODE._phase.ordinal() > CodeGen.Phase.FinalizeIR.ordinal() ) + return Parser.error("Accessing unknown field '"+_name+"' from '*"+tmp._obj._name+"'",_loc); + // Sane field + if( off() instanceof ConFldOffNode && CodeGen.CODE._phase.ordinal() > CodeGen.Phase.FinalizeIR.ordinal() ) + return Parser.error("Accessing unknown field '"+_name+"' from '*"+tmp._obj._name+"'",_loc); + return null; + } + + // Memory writers which must follow this read. A packaging node is not a + // write; follow this alias through aggregates to its writers. + public Ary<Node> antiDeps( Ary<Node> antideps ) { + antideps.clear(); + return antiDeps(mem(),antideps); + } + + private Ary<Node> antiDeps( Node mem, Ary<Node> antideps ) { + for( Node use : mem._outputs ) { + if( use instanceof MemMergeNode merge ) { + if( merge.alias(_alias)==mem ) + antiDeps(merge,antideps); + } else if( use instanceof CallNode || use instanceof CLInitNode || + (use instanceof MemOpNode st && !st._isLoad && st._alias==_alias) || + (use instanceof EscapeNode esc && esc.fld()._alias==_alias) || + (use instanceof MemPhiNode phi && phi._alias==_alias) || + (use instanceof BulkMemPhiNode phi && !phi.isSplit(_alias)) ) { + if( !antideps.contains(use) ) antideps.add(use); + } + } + return antideps; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java new file mode 100644 index 00000000..5ae716a7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** A Phi for one precise memory alias. */ +public class MemPhiNode extends PhiNode { + + public final int _alias; + + public MemPhiNode(String label, int alias, Node... inputs) { + super(label, inputs); + assert alias > 1; + _alias = alias; + } + + public MemPhiNode(MemPhiNode phi) { + super(phi,phi._label); + _alias = phi._alias; + } + + + @Override + public Type compute() { + Type t = super.compute(); + assert BulkMemPhiNode.checkMem(this,_alias); + if( !(t instanceof TypeMem tmem) ) return t; + assert tmem._alias==1 || tmem._alias==_alias; + // This Phi structurally knows its alias, but not whether the field is + // final. Do not infer slice finality from evolving input types. + return TypeMem.make(_alias,tmem._t,tmem._one,tmem._clz,false,tmem._escFs,tmem._escAs); + } + + @Override + public Node idealize() { + // A precise memory Phi can always bypass a MemMerge and select its + // matching slice. + for( int i=1; i<nIns(); i++ ) { + if( in(i) instanceof MemMergeNode mem ) { + setDef(i,mem.alias(_alias)); + return this; + } + Node slice = CLInitNode.slice(in(i),_alias,this); + if( slice!=in(i) ) { setDef(i,slice); return this; } + } + if( nIns()==3 && region() instanceof LoopNode loop && !loop.inProgress() && + in(2) instanceof StoreNode st && st.mem()==this ) { + Node sink = st.sink(this); + if( sink!=null ) return sink; + } + return super.idealize(); + } + + @Override public boolean eq(Node n) { + return _alias==((MemPhiNode)n)._alias && super.eq(n); + } + + @Override int hash() { return _alias; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/MinusNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MinusNode.java new file mode 100644 index 00000000..4d7e3ddb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MinusNode.java @@ -0,0 +1,77 @@ +package com.seaofnodes.simple.node; + + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class MinusNode extends Node implements ModeNode { + // Mode is 0 for unknown, 1 for int, 2 for flt + byte _mode; + + public MinusNode(Node in) { super(null, in); } + public MinusNode(Node in, byte mode) { super(null, in); _mode = mode; } + + + @Override public byte mode() { return _mode; } + @Override public Node setMode(byte mode) { + assert _mode==0 && (mode==1 || mode==2); + unlock(); // _mode participates in GVN hash and equality + _mode = mode; + return this; + } + + @Override protected String format() { return "(-%1)"; } + + @Override + public Type compute() { + Type t = in(1)._type; + if( t==null ) + return Type.TOP; + if( t.isHigh() ) + return _mode==0 ? Type.TOP : + _mode==1 ? TypeInteger.TOP : + t instanceof TypeFloat ? t : TypeFloat.F64.dual(); + byte mode = _mode; + if( mode==0 ) { + if( t instanceof TypeFloat ) mode=2; + if( t instanceof TypeInteger ) mode=1; + } + if( mode==0 ) + return CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal() ? Type.BOTTOM : Type.TOP; + if( mode==1 ) { + if( t instanceof TypeInteger i0 && + i0._min != Long.MIN_VALUE && i0._max != Long.MIN_VALUE ) + return TypeInteger.make(-i0._max,-i0._min,i0._widen); + return TypeInteger.BOT; + } + if( t instanceof TypeFloat f ) + return f.isConstant() ? TypeFloat.constant(-f.value()) : f; + return TypeFloat.F64; + } + + @Override + public Node idealize() { + // -(-x) is x + if( in(1) instanceof MinusNode minus && addDep(minus.in(1))._type.isa(compute()) ) + return minus.in(1); + + // Can we decide int vs flt? + if( _mode==0 ) { + byte mode = (byte)( + in(1)._type instanceof TypeInteger ? 1 : + in(1)._type instanceof TypeFloat ? 2 : 0); + if( mode!=0 ) return setMode(mode).init(); + } + + return null; + } + @Override public Parser.ParseException err() { + return ArithNode.nodeErr(this,null,"-",_mode,true); + } + @Override public boolean eq( Node n ) { return _mode==((MinusNode)n)._mode; } + @Override public int hash() { return _mode; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ModeNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ModeNode.java new file mode 100644 index 00000000..dc635447 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ModeNode.java @@ -0,0 +1,12 @@ +package com.seaofnodes.simple.node; + +/** + * A node whose numeric input family is selected once: unknown, integer, or + * floating point. Implementors remain ordinary Nodes; this interface only + * centralizes the structural mode contract shared by arithmetic, comparisons, + * and unary minus. + */ +public interface ModeNode { + byte mode(); + Node setMode(byte mode); +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/MulNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MulNode.java new file mode 100644 index 00000000..49c6432a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MulNode.java @@ -0,0 +1,65 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.BitSet; + +public class MulNode extends ArithNode { + @Override boolean allowFloat() { return true; } + public MulNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + public MulNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs, mode); } + + + @Override public String op() { return "*"; } + + @Override long doOp( long x, long y ) { return x * y; } + @Override double doOp(double x, double y ) { return x * y; } + @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { + 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._type.isa(compute()) ? lhs : super.idealize(); + if( c==0 ) return CodeGen.CODE.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 super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new MulNode(lhs,rhs,_mode); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/MultiNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MultiNode.java new file mode 100644 index 00000000..152d525a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/MultiNode.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.util.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, Node dep) { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/NeverNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NeverNode.java new file mode 100644 index 00000000..a7b4cf65 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NeverNode.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeTuple; + + +// "Never true" for infinite loop exits +public class NeverNode extends IfNode { + public NeverNode(NeverNode ctrl) { super(ctrl); } + public NeverNode(float ignore, Node ctrl) { super(ctrl,null); } + + + @Override public String label() { return "Never"; } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p("Never"); } + + @Override public Type compute() { return TypeTuple.IF_BOTH; } + + @Override public Node idealize() { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/NewNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NewNode.java new file mode 100644 index 00000000..5cbe1b61 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NewNode.java @@ -0,0 +1,98 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + * Allocation! Allocate a chunk of memory, no init. + * Takes in ctrl and size, and TypeStruct. + * Produces a ptr and private memory. +*/ +public class NewNode extends Node implements MultiNode { + public TypeStruct _ts; + public NewNode( TypeStruct ts, Node ctrl, Node size ) { super(ctrl,size); _ts = ts; } + + public Node ctrl() { return in(0); } + public Node size() { return in(1); } + + // --- Serialization + public NewNode(NewNode nnn) { super(nnn); _ts = nnn._ts; } + + + @Override public String label() { return "new_"+(_ts.isAry() ? "ary_"+_ts._fields[1]._t.str() : _ts.str()); } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.p("new ").p(_ts.str()); + } + + @Override public TypeTuple compute() { + if( ctrl()._type.isHigh() ) + return TypeTuple.DEAD_NEW; + TypeStruct ts = _ts.isAry() ? _ts : _ts.makeInit(); + return TypeTuple.make(TypeMemPtr.make(_ts), + // Arrays get pre-zeroed by constructor. + // Objects start with default-zero memory for + // fields that can be zero-initialized. This is + // still private memory, so no aliases escape yet. + TypeMem.make(1,ts,true,false,false,null,null)); + } + + @Override public Node idealize() { return null; } + + @Override boolean _upgradeType( HashMap<String,Type> TYPES) { + Type old = _ts; _ts = (TypeStruct)_ts.upgradeType(TYPES); return old != _ts; + } + + // Two NewNodes are never equal; NewNodes always make independent pointers + // so they are not functional - they internally bump-ptr allocate always a + // new pointer. + @Override public boolean eq(Node n) { return this==n; } + + @Override int hash() { return _ts.hashCode(); } + + + @Override public Parser.ParseException err() { + return _ts._fref ? Parser.error("Unknown struct type '"+_ts._name+"'",null) : null; + } + + + // ------------ + // MachNode specifics, shared across all CPUs + public int _arg2Reg, _xslot; + private RegMask _arg3Mask; + private RegMask _retMask; + private RegMask _kills; + public void cacheRegs(CodeGen code) { + _arg2Reg = code._mach.callArgMask(TypeFunPtr.CALLOC,2,0,code._cCallingConv).firstReg(); + _arg3Mask = code._mach.callArgMask(TypeFunPtr.CALLOC,3,0,code._cCallingConv); + // Return mask depends on TFP (either GPR or FPR) + _retMask = code._mach.retMask(TypeFunPtr.CALLOC); + // Kill mask is all caller-saves, and any mirror stack slots for args + // in registers. + RegMaskRW kills = code._cCallerSave.copy(); + // Start of stack slots + int maxReg = code._mach.regs().length; + // Incoming function arg slots, all low numbered in the RA + int fslot = cfg0().fun()._maxArgSlot; + // Killed slots for this calls outgoing args + int xslot = code._mach.maxArgSlot(TypeFunPtr.CALLOC,code._cCallingConv); + _xslot = (maxReg+fslot)+xslot; + for( int i=0; i<xslot; i++ ) + kills.set((maxReg+fslot)+i); + _kills = kills; + } + public String op() { return "alloc"; } + public RegMask regmap(int i) { return i==1 ? _arg3Mask : null; } + public RegMask outregmap() { return null; } + public RegMask outregmap(int idx) { return idx==0 ? _retMask : null; } + public RegMask killmap() { return _kills; } + public void asm(CodeGen code, SB sb) { + sb.p("#malloc, ").p(code.reg(size())); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/Node.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/Node.java new file mode 100644 index 00000000..de39ca80 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/Node.java @@ -0,0 +1,696 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.BaseNode; + +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.print.IRPrinter; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.util.*; +import java.util.function.Function; +import static com.seaofnodes.simple.codegen.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 extends BaseNode<Node> implements Cloneable { + + /** + * 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 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 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) { + super(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 ) { + super(CODE.getUID()); // allocate unique dense ID + assert CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Select.ordinal(); + _inputs = new Ary<>(n==null ? new Node[0] : n._inputs.asAry()); + _outputs = new Ary<>(Node.class); + _type = n==null ? Type.BOTTOM : n._type; + _deps = null; + _hash = 0; + } + + // Easy reading label for debugger, e.g. "Add" or "Region" or "EQ" + public String label() { return getClass().getSimpleName().replace("Node",""); } + + public String p(int depth) { return IRPrinter.prettyPrint(this,depth); } + + public boolean isConst() { return false; } + + // ------------------------------------------------------------------------ + // Graph Node & Edge manipulation + + @Override public String typeName() { return _type==null ? null : _type.toString(); } + + /** + * 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; + } + public <N extends Node> N setDefX(int idx, N new_def ) { + while( nIns() <= idx ) addDef(null); + return setDef(idx,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. + public 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 + public void insertDef(int idx, Node new_def) { + _inputs.insert(null,idx); + 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 + @SuppressWarnings("unchecked") + 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. + public boolean delUse( Node use ) { + _outputs.del(_outputs.find(use)); + moveDepsToWorklist(); // User-count and anti-dependence queries can now change. + return _outputs.isEmpty(); + } + + // Shortcut for "popping" until n nodes. A "pop" is basically a + // setDef(last,null) followed by lowering the nIns() count. + public 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 + } + } + public Node removeLast() { + Node old_def = _inputs.pop(); + if( old_def != null ) + old_def.delUse(this); + return 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( ) { + assert isUnused(); // Has no uses, so it is dead + unlock(); + moveDepsToWorklist(); + _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 ) { + if( CODE.add(old_def).delUse(this) ) + old_def.kill(); // If we removed the last use, the old def is now dead + old_def.moveDepsToWorklist(); // Use-count changes can enable distant rewrites. + } + } + assert isDead(); // Really dead now + } + + // Preserve CFG use-ordering when killing + public void killOrdered() { + CFGNode cfg = cfg0(); + cfg._outputs.remove(cfg._outputs.find(this)); + _inputs.set(0,null); + kill(); + } + + // 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. + @SuppressWarnings("unchecked") + 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(); + } + public void isKill() { + if( 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.add(n); + CODE.addAll(n._outputs); + } + kill(); + } + + // insert `this` immediately after `def` in the same basic block. + public void insertAfter( Node def ) { + CFGNode cfg = def.cfg0(); + int i = cfg._outputs.find(def)+1; + if( cfg instanceof CallEndNode ) { + cfg = cfg.uctrl(); i=0; + } else if( def.in(0) instanceof MultiNode ) { + assert i==0; + i = cfg._outputs.find(def.in(0))+1; + } + + while( cfg.out(i) instanceof PhiNode || cfg.out(i) instanceof CalleeSaveNode ) i++; + cfg._outputs.insert(this,i); + _inputs.set(0,cfg); + } + + // Insert this in front of use.in(uidx) with this, and insert this + // immediately before use in the basic block. + public void insertBefore( Node use, int uidx ) { + CFGNode cfg = use.cfg0(); + int i; + if( use instanceof PhiNode phi ) { + cfg = phi.region().cfg(uidx); + if( cfg instanceof CProjNode && cfg.in(0) instanceof NeverNode nvr ) + cfg = nvr.cfg0(); + i = cfg.nOuts()-1; + } else { + i = cfg._outputs.find(use); + } + cfg._outputs.insert(this,i); + _inputs.set(0,cfg); + if( _inputs._len > 1 && this instanceof SplitNode ) + setDefOrdered(1,use.in(uidx)); + use.setDefOrdered(uidx,this); + } + + public void setDefOrdered( int idx, Node def) { + // If old is dying, remove from CFG ordered + Node old = in(idx); + if( old!=null && old.nOuts()==1 ) { + CFGNode cfg = old.cfg0(); + if( cfg!=null ) { + cfg._outputs.remove(cfg._outputs.find(old)); + old._inputs.set(0,null); + } + } + setDef(idx,def); + } + + public void removeSplit() { + CFGNode cfg = cfg0(); + cfg._outputs.remove(cfg._outputs.find(this)); + _inputs.set(0,null); + if( _inputs._len > 1 ) subsume(in(1)); + } + + // ------------------------------------------------------------------------ + // 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( ) { + var obs = CODE._midAssert ? null : CODE._obs; + if( obs != null ) obs.before(this); + Node n = peepholeOpt(); + Node rez = n==null ? this : deadCodeElim(n._nid >= _nid ? n.peephole() : n); + if( obs != null ) obs.after(this, n==null ? null : rez, false); + return rez; // 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( !isConst() && !(this instanceof ParmNode parm && parm.inProgress()) && + _type.isHighOrConst() ) + 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) : "Monotonicity test failed: "+this+"#"+_nid+" old="+old+" new="+type; + //assert !type.hasClosedBuilder0(); // New types should not have closed builders + if( old == type ) return old; + _type = type; // Set _type late for easier assert debugging + CODE.addAll(_outputs); + moveDepsToWorklist(); + return old; + } + + @SuppressWarnings("unchecked") + 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; + public int nDeps() { return _deps == null ? 0 : _deps.size(); } + public Node dep(int idx) { return _deps.at(idx); } + + private boolean addDepImpl( Node dep ) { + // Running peepholes during the big assert cannot have side effects + // like adding dependencies. + if( CODE._midAssert ) return false; + if( CODE._obs != null ) CODE._obs.dep(this, dep); + if( dep._deps==null ) dep._deps = new Ary<>(Node.class); + if( dep._deps .find(this) != -1 ) return false; // Already on list + if( dep._outputs.find(this) != -1 ) return false; + return true; + } + + /** + * Add a node to the list of dependencies. Only add it if it's 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. + */ + public <N extends Node> N addDep( N dep ) { + if( !addDepImpl(dep) ) return dep; + if( dep._inputs .find(this) != -1 ) return dep; // No need for deps on immediate neighbors + dep._deps.add(this); + return dep; + } + + public <N extends Node> N addDepForwards( N dep ) { + if( !addDepImpl(dep) ) return dep; + dep._deps.add(this); + return dep; + } + + // Move the dependents onto a worklist, and clear for future dependents. + public void moveDepsToWorklist( ) { moveDepsToWorklist(CODE._iter); } + public void moveDepsToWorklist( IterPeeps iter ) { + if( _deps == null ) return; + iter.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. + public 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; } + + // ------------------------------------------------------------------------ + // Peephole utilities + + // Swap inputs without letting either input go dead during the swap. + public 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()) ) { + dep.addDep(in(i)); // 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"); } + + public Node copy() { return copyEmpty(); } + + // Exact-class copy with fresh identity and no edges. Unlike machine + // rematerialization copies, this never registers or copies input edges. + public final Node copyEmpty() { + Node n; + try { n = (Node)clone(); } + catch( Exception e ) { throw new RuntimeException(e); } + n._nid = CODE.getUID(); // allocate unique dense ID + n._inputs = new Ary<>(Node.class); + n._outputs = new Ary<>(Node.class); + n._deps = null; + n._hash = 0; + if( n instanceof CallNode call ) CODE._calls.add(call); + return n; + } + + // Report any post-optimize errors + public Parser.ParseException err() { return null; } + + // Common integer constants + public static ConstantNode con(long x) { return (ConstantNode)ConstantNode.make(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; + } + + + // If a Type upgrade happens, put on worklist + boolean _upgradeType( HashMap<String,Type> TYPES ) { return false; } + public final void upgradeType( HashMap<String,Type> TYPES ) { + Type old = _type; _type = _type.upgradeType(TYPES); + boolean progress = _upgradeType(TYPES); + if( progress || old != _type ) { + CODE.add(this); + CODE.addAll(_outputs); + if( _deps!=null ) CODE.addAll(_deps); + } + } + + /** + * Debugging utility to find a Node by index + */ + public Node find(int nid) { return _find(nid, new BitSet()); } + private Node _find(int nid, BitSet bs) { + if( bs.get(_nid) ) return null; // Been there, done that + bs.set(_nid); + if( _nid==nid ) return this; + Node x; + for( Node def : _inputs ) if( def != null && (x = def._find(nid,bs)) != null ) return x; + for( Node use : _outputs ) if( use != null && (x = use._find(nid,bs)) != null ) return x; + return null; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/NotNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NotNode.java new file mode 100644 index 00000000..e22d16b6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/NotNode.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node; + + +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +public class NotNode extends Node { + public NotNode(Node in) { super(null, in); } + + + @Override protected String format() { return "(!%1)"; } + + @Override + public TypeInteger compute() { + Type t0 = in(1)._type; + if( t0.isHigh() ) return (TypeInteger)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/chapter-modules/src/main/java/com/seaofnodes/simple/node/OrNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/OrNode.java new file mode 100644 index 00000000..263c3e87 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/OrNode.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +public class OrNode extends ArithNode { + public OrNode( Parser.Lexer loc, Node lhs, Node rhs) { super(loc,lhs, rhs, (byte)1); } + + + @Override public String label() { return "Or"; } + @Override public String op() { return "|"; } + @Override long doOp( long x, long y ) { return x | y; } + @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { + 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 && t1.isa(TypeInteger.BOT) && forceInt(lhs) ) + 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 super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new OrNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ParmNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ParmNode.java new file mode 100644 index 00000000..316efb93 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ParmNode.java @@ -0,0 +1,95 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class ParmNode extends PhiNode { + + // Argument indices are mapped one-to-one on CallNode inputs + public final int _idx; // Argument index + public Type _declaredType; + + public ParmNode(String label, int idx, Type declaredType, Node... inputs) { + super(label, inputs); + _idx = idx; + _declaredType = declaredType; + _type = declaredType; + } + public ParmNode(ParmNode parm) { super(parm, parm._label ); _idx = parm._idx; _declaredType = parm._declaredType; } + + + @Override public String label() { return MemOpNode.mlabel(_label); } + + @Override boolean _upgradeType( HashMap<String,Type> TYPES) { + Type t = _declaredType.upgradeType(TYPES); + if( t == _declaredType ) return false; + unlock(); + _declaredType = t; + return true; + } + + public FunNode fun() { return (FunNode)in(0); } + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + if( fun()._name!=null && fun()._name.endsWith("<clinit>") && _label.equals("arg") ) + return p.p("arg"); + p.p("Parm_").p(_label).open(); + for( Node in : _inputs ) p.n(in).p(","); + return p.unchar(',').close(); + } + + + // Always in-progress until we run out of unknown callers + @Override public boolean inProgress() { return in(0) instanceof FunNode fun && fun.inProgress(); } + + @Override + public Type compute() { + if( !(region() instanceof RegionNode r) ) + return region()._type==Type.XCONTROL || region()._type==Type.TOP ? (_type instanceof TypeMem ? TypeMem.TOP : Type.TOP) : _type; + // During parsing Phis have to be computed type pessimistically. + if( r.inProgress() || in(nIns()-1)==null ) + return _declaredType; + // Set type to local top of the starting type + Type t = 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. + Type ctrl = addDep(r.in(i))._type; + if( ctrl != Type.XCONTROL && ctrl != Type.TOP ) { + if( in(i)._type==Type.BOTTOM ) + return Type.BOTTOM; + t = t.meet(in(i)._type); + } + } + // Recursion is a loop in disguise. A Parm merges the first argument + // with those from recursive calls, just as a loop Phi merges its entry + // and backedge. Use the widening rule in PhiNode.compute here too. + // CallEndNode.compute handles the separate cycle through return values; + // widening this Parm alone cannot stop f(x-1)+1 from growing its result. + if( t instanceof TypeInteger ti && !ti.isHigh() && !ti.isConstant() ) + t = ti.same_but_slightly_wider_than(); + return t.join(_declaredType); + } + + @Override + public Node idealize() { + if( !(region() instanceof FunNode) ) + // A dead function can lose every caller before the Parm itself is + // removed; there is no replacement input in that transient form. + return nIns()==1 ? null : in(1); // Input has collapse to e.g. starting control. + // If function is folding, do all possible peeps + if( fun()._folding ) return super.idealize(); + + // Skip most phi optimizations on parms + return null; + } + + @Override public boolean eq( Node n ) { + return ((ParmNode)n)._idx==_idx && super.eq(n); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PhiNode.java new file mode 100644 index 00000000..61e4b468 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PhiNode.java @@ -0,0 +1,275 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class PhiNode extends Node { + + public final String _label; + + public PhiNode( String label, Node... inputs) { + super(inputs); + _label = label; + } + public static PhiNode make(String label, Type minType, Node... inputs) { + return minType instanceof TypeMem mem + ? mem._alias==1 + ? new BulkMemPhiNode(label,inputs) + : new MemPhiNode(label,mem._alias,inputs) + : new PhiNode(label, inputs); + } + // Used by ParmNode + public PhiNode(PhiNode phi, String label) { super(phi); _label = label; } + // Used by instruction Selection + public PhiNode(PhiNode phi) { this(phi,phi._label); } + // Used by the infinite-loop exit breaker + public PhiNode(RegionNode r, Node sample) { + super(new Node[]{r}); + _label = ""; + _type = sample._type; + while( nIns() < r.nIns() ) + addDef(sample); + } + + + @Override public String label() { return "Phi_"+MemOpNode.mlabel(_label); } + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + if( !(region() instanceof RegionNode r) || r.inProgress() ) + p.p("Z"); + p.p("Phi("); + for( Node in : _inputs ) p.n(in).p(","); + return p.unchar(',').close(); + } + + public CFGNode region() { return (CFGNode)in(0); } + @Override public boolean isMem() { return _type instanceof TypeMem; } + + @Override + public Type compute() { + if( !(region() instanceof RegionNode r) ) + return Type.TOP; + // During parsing Phis have to be computed type pessimistically. + if( r.inProgress() || in(nIns()-1)==null ) + return Type.BOTTOM; + // Set type to local top of the starting type + Type t = 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. + Type ctrl = addDep(r.in(i))._type; + if( ctrl != Type.XCONTROL && ctrl != Type.TOP ) { + if( in(i)._type==Type.BOTTOM ) + return Type.BOTTOM; + t = t.meet(in(i)._type); + } + } + + // Widen loop Phis so integer ranges cannot grow one step per trip forever. + // Recursion is a loop in disguise: ParmNode.compute widens arguments, + // and CallEndNode.compute widens results. Both are needed because the + // recursive result can feed itself without passing through a Parm. + // Above-center ranges may still fall to unwidened constants. + // Widen only below-center ranges to preserve monotonicity. + if( r instanceof LoopNode && // Only around loops + t instanceof TypeInteger ti && // Only widen integers + !ti.isHigh() && !ti.isConstant() ) { // Only widen live ranges + // Widen, to prevent infinite falling of TypeIntegers + return ti.same_but_slightly_wider_than(); + } + + return t; + } + + @Override + public Node idealize() { + RegionNode r = (RegionNode)region(); + if( r.inProgress() || nOuts()==0 ) + 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; + + // Generic "pull down op" + Node progress; + if( same_op() && (progress = drop_same_op()) != null ) + return progress; + + // If merging Phi(ZERO, guardNZ(N)) at the matching `if(N)` join, the + // Phi is exactly N. The guard arm proves N is non-zero, and the other + // arm contributes N's zero value. This is only legal if N is already + // available at the Phi region; otherwise a return-scope/live-on-exit + // Phi can lose the zero arm and export a branch-local value upward. + Node unguard; + if( (unguard=matchingGuardMerge(r,1)) != null ) return unguard; + if( (unguard=matchingGuardMerge(r,2)) != null ) return unguard; + + return null; + } + + private Node matchingGuardMerge(RegionNode r, int nzIdx) { + Node zero = in(3-nzIdx); + if( !(in(nzIdx) instanceof GuardNode cast && cast._nonZero) || + zero._type.makeZero()!=zero._type || + cast.in(1)==this ) + return null; + if( !(r.in(nzIdx) instanceof CProjNode nz && nz._idx==0 && nz.ctrl() instanceof IfNode iff) ) + return null; + if( !(r.in(3-nzIdx) instanceof CProjNode z && z._idx==1 && z.ctrl()==iff) ) + return null; + // Predicate was guarded? + Node n = cast.in(1), tested = null; + while( true ) { + if( iff.pred()==n ) tested = n; + if( !(n instanceof GuardNode guard && guard._nonZero) ) break; + n = guard.in(1); + } + CFGNode early = CFGNode.earlyCFG(n); + return tested!=null && (early==null || early.dominates(r,this)) ? n : null; + } + + // Same op on all Phi paths; all ops have only the Phi as a use. + // None have a control input. + private boolean same_op() { + Node op = in(1); + if( op instanceof EscapeNode || op instanceof CFGNode || op instanceof ConstantNode || op instanceof PhiNode || + op instanceof ProjNode || op instanceof NewNode || op instanceof ScopeNode || op instanceof MemMergeNode ) return false; + // A bulk Phi must split its aliases before factoring precise Stores. + if( this instanceof BulkMemPhiNode ) return false; + // Bulk splitting currently identifies parallel slices by Region/alias. + // Factoring can introduce another slice at a different memory point; + // wait until that Region's bulk partitioning has finished. + if( op instanceof MemOpNode ) + for( Node use : region()._outputs ) + if( use instanceof BulkMemPhiNode ) { + addDepForwards(use); + return false; + } + Node busy = null; + for( int i=1; i<nIns(); i++ ) { + Node n = in(i); + if( addDep(region().in(i))._type==Type.XCONTROL || + op.getClass()!=n.getClass() || n.nIns()!=op.nIns() || n.in(0)!=null || !op.eq(n) ) return false; + if( n instanceof MemOpNode mem && !mem.canDrop((MemOpNode)op,this) ) return false; + // Moving a Store must remove the old effect, not duplicate it. + // Allow one shared arm: the other arms disappear into the single + // factored operation, usually reducing the total operation count. + addDepForwards(n); + if( n.nOuts()>1 ) { + for( Node use : n._outputs ) + if( use!=null && use!=this ) addDepForwards(use); + if( n instanceof StoreNode || busy!=null ) return false; + busy=n; + } + for( int j=1; j<n.nIns(); j++ ) + if( n.in(j) instanceof ScopeNode || ((n.in(j)==null) != (op.in(j)==null)) ) return false; + } + return true; + } + + private Node drop_same_op() { + Node op = in(1), cp = op.copyEmpty(); + cp._type = null; + cp.addDef(null); + for( int j=1; j<op.nIns(); j++ ) { + Node x = op.in(j); + boolean different = false; + for( int i=2; i<nIns(); i++ ) + if( in(i).in(j)!=x ) different=true; + if( different ) { + Node[] ins = new Node[nIns()]; + ins[0] = region(); + Type t = Type.TOP; + for( int i=1; i<nIns(); i++ ) { + ins[i] = in(i).in(j); + t = t.meet(ins[i]._type); + } + // Do not introduce an operand Phi mixing incompatible type families. + if( t==Type.BOTTOM ) { + for( int i=1; i<ins.length; i++ ) addDep(ins[i]); + cp.kill(); + return null; + } + PhiNode phi = j==1 && op instanceof MemOpNode mem + ? new MemPhiNode(_label,mem._alias,ins) + : PhiNode.make(_label,t,ins); + x = phi.peephole(); + } + cp.addDef(x); + } + // Factoring must not widen the result (e.g. correlated And operands). + if( cp.compute().isa(compute()) ) return cp; + for( int i=1; i<nIns(); i++ ) + for( int j=1; j<in(i).nIns(); j++ ) + if( in(i).in(j)!=null ) addDep(in(i).in(j)); + cp.kill(); + return null; + } + + /** + * 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( addDep(region().in(i))._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. + dep.addDep(this); + 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 public boolean eq( Node n ) { + return !inProgress() && super.eq(n); + } + + @Override + public Parser.ParseException err() { + if( _type != Type.BOTTOM ) return null; + + // Global BOTTOM retains the same role for non-scalar families. + 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; + + SB sb = new SB().p("No common type amongst "); + for( int i=1; i<nIns(); i++ ) + sb.p(in(i)._type.toString()).p(" and "); + return Parser.error(sb.unchar(5).toString(),null); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/Proj.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/Proj.java new file mode 100644 index 00000000..c8b9378f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/Proj.java @@ -0,0 +1,2 @@ +package com.seaofnodes.simple.node; +public interface Proj { int idx(); } diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ProjNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ProjNode.java new file mode 100644 index 00000000..a0142379 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ProjNode.java @@ -0,0 +1,59 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeTuple; +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class ProjNode extends Node implements Proj { + + // 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 + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + if( _label != null ) return p.p(_label); + if( in(0) instanceof CallEndNode cend && cend.call()!=null ) + return p.n(cend.call()); + return p.p("LONELY PROJ"); + } + + @Override public CFGNode cfg0() { + return in(0) instanceof CFGNode cfg ? cfg : in(0).cfg0(); + } + + @Override public boolean isMem() { return _type instanceof TypeMem; } + + @Override + public Type compute() { + Type t = in(0)._type; + return t instanceof TypeTuple tt ? tt._types[_idx] : t.oob(); + } + + @Override public Node idealize() { return ((MultiNode)in(0)).pcopy(_idx,this); } + + @Override + public boolean eq( Node n ) { return _idx == ((ProjNode)n)._idx; } + + @Override + int hash() { return _idx; } + + @Override public int idx() { return _idx; } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntMach.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntMach.java new file mode 100644 index 00000000..9a9b94fd --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntMach.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.util.SB; + +/** Zero-code machine form of {@link PtrToIntNode}. */ +public class PtrToIntMach extends PtrToIntNode implements MachNode { + public PtrToIntMach(PtrToIntNode ptr) { super(ptr); } + @Override public String op() { return label(); } + @Override public RegMask regmap(int i) { assert i==1; return RegMask.FULL; } + @Override public RegMask outregmap() { return RegMask.FULL; } + @Override public int twoAddress() { return 1; } + @Override public void encoding(Encoding enc) { } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(in(1))); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java new file mode 100644 index 00000000..094734fa --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java @@ -0,0 +1,36 @@ +package com.seaofnodes.simple.node; + + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMemPtr; + + +/** + * Reinterpret a non-null managed pointer as its raw integer address. + * + * This node changes representation families but emits no code. No + * control-dependent proof can turn a pointer into an integer; array-to-C-pointer + * conversion adds the array body offset separately. + */ +public class PtrToIntNode extends Node { + public PtrToIntNode(Node ptr) { super(null,ptr); } + public PtrToIntNode(PtrToIntNode ptr) { super(ptr); } + + + @Override protected String format() { return "(i64*)%1"; } + + @Override public Type compute() { + return in(1)._type.isHigh() ? TypeInteger.TOP : TypeInteger.BOT; + } + + @Override public Node idealize() { return null; } + + @Override public Parser.ParseException err() { + Type t = in(1)._type; + if( t instanceof TypeMemPtr ptr ) + return ptr.notNull() ? null : Parser.error("Cannot convert a nullable pointer to an integer",null); + return Parser.error("Cannot convert "+t.str()+" to an integer pointer",null); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyMach.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyMach.java new file mode 100644 index 00000000..a35f5a1d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyMach.java @@ -0,0 +1,19 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.util.SB; + +/** + * Cast a pointer to read-only + */ +public class ReadOnlyMach extends ReadOnlyNode implements MachNode { + public ReadOnlyMach( ReadOnlyNode n ) { super(n); } + @Override public String op() { return label(); } + @Override public RegMask regmap(int i) { assert i==1; return RegMask.FULL; } + @Override public RegMask outregmap() { return RegMask.FULL; } + @Override public int twoAddress( ) { return 1; } + @Override public void encoding( Encoding enc ) { } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(in(1))); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java new file mode 100644 index 00000000..ab7e0ae0 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node; + + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeMemPtr; + +/** + * 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 protected String format() { return "(const)%1"; } + @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/chapter-modules/src/main/java/com/seaofnodes/simple/node/RegionNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/RegionNode.java new file mode 100644 index 00000000..923773c4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/RegionNode.java @@ -0,0 +1,172 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +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); if( r!=null ) _loc = r._loc; } + + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.p(label()); + } + + @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(false); + 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() && nIns()>3 && // No Phi users, just a control user + in(1) instanceof CProjNode p1 && + in(2) instanceof CProjNode p2 && + addDepForwards(p1).in(0)==addDepForwards(p2).in(0) && + 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); + } + + // Flatten stacked regions (no loops involved) + if( getClass() == RegionNode.class && nIns()>2 ) { + for( int i=1; i<nIns(); i++ ) + if( cfg(i) instanceof RegionNode region && region.getClass() == RegionNode.class && region.nIns()>2 && !hasMidUser(region) ) { + assert !region.inProgress(); + // Fold Phis + for( Node use : _outputs ) { + if( use instanceof PhiNode phi ) { + PhiNode phi2 = phi.in(i) instanceof PhiNode phi2x && phi2x.region()==region ? phi2x : null; + for( int j=1; j<region.nIns(); j++ ) + phi.addDef(phi2==null ? phi.in(i) : phi2.in(j)); + CodeGen.CODE.add(phi); + } + } + + // Fold Region + for( int j=1; j<region.nIns(); j++ ) + addDef(region.in(j)); + setDef(i,CodeGen.CODE.XCTRL); + return this; + } + } + + return null; + } + + Node deadPath(boolean skipStart) { + // Delete dead paths into a Region + int path = findDeadInput(skipStart ? 2 :1); + 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; + return removeDeadPath(path); + } + + public Node removeDeadPath(int path) { + // 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.in(path)._outputs); + CodeGen.CODE.addAll(phi.delDef(path)._outputs); + } + } + if( !isDead() ) delDef(path); + return isDead() ? CodeGen.CODE.XCTRL : this; + } + + private int findDeadInput(int startIdx) { + for( int i=startIdx; 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; + } + + // Is there a value in-between 2 Regions? Difficult to correctly fuse + // without extensive testing, so just ignore. + boolean hasMidUser(RegionNode r) { + for( Node use : r._outputs ) { + if( use==this ) continue; + if( use instanceof PhiNode ) { + for( Node data : use._outputs ) { + if( !(data instanceof PhiNode phi2) || phi2.region()!=this ) + { addDep(use); if( data!=null ) addDep(data); return true; } + } + } else + { addDep(use); return true; } // Control user + } + return false; + } + + + // Immediate dominator of Region is a little more complicated. + @Override public int idepth() { + if( validIDepth() ) return _idepth; + int depth=0; + for( Node n : _inputs ) + if( n!=null ) + depth = Math.max(depth,((CFGNode)n).idepth()+1); + return cacheIDepth(depth); + } + + @Override public CFGNode idom(Node dep) { + CFGNode lca = null; + // Recompute from predecessors: CFG edits can change the dominator. + for( int i=1; i<nIns(); i++ ) + if( !cfg(i)._type.isHigh() ) + lca = cfg(i).domLCA(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 public boolean eq( Node n ) { return !inProgress(); } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReturnNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReturnNode.java new file mode 100644 index 00000000..0bb090ee --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ReturnNode.java @@ -0,0 +1,129 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +/** + * 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); } + @Override public FunNode fun() { return _fun; } + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + p.p("return "); + if( expr()==null ) p.p("----"); + else p.n(expr()); + return p.p(";"); + } + + // No one unique control follows; can be many call end sites + @Override public CFGNode uctrl() { return null; } + + @Override + public Type compute() { + return TypeTuple.make(ctrl()._type,mem()._type,expr()._type); + } + + @Override public Node idealize() { + if( _fun.isDead() ) return null; + + // Upgrade signature based on return type + Type ret = expr()._type; + TypeFunPtr fcn = _fun.sig(); + if( ret != fcn.ret() && ret.isa(fcn.ret()) ) + _fun.setSig(fcn.makeFrom(ret)); + + return null; + } + + @Override public Parser.ParseException err() { + if( ctrl()._type == Type.CONTROL && + expr()._type == Type.TOP ) + return Parser.error("No defined return type",null); + if( ctrl()._type == Type.CONTROL && + expr()._type == TypeScalar.BOT ) + return Parser.error("Cannot return generic scalar",null); + // With no user constructor, <init> doubles as the public no-arg + // constructor. Required fields remain TOP in its private memory: a + // typed parser poison which deliberately emitted no Store or code. + if( ctrl()._type == Type.CONTROL && _fun.isInstance() ) { + TypeStruct self = ((TypeMemPtr)_fun.sig().arg(0))._obj; + if( !CodeGen.hasUserConstructor(self) && expr()._type instanceof TypeMem mem ) + for( Field fld : self._fields ) { + if( !(fld._t instanceof TypeNil tn && tn.notNull()) ) continue; + Type actual = mem._alias==1 && mem._t instanceof TypeStruct ts + ? ts.field(fld._fname)._t + : mem._alias==fld._alias ? mem._t : Type.TOP; + if( actual==Type.TOP || actual instanceof TypeNil atn && atn.nullable() ) + return Parser.error("'"+self._name+"' is not fully initialized, field '"+ + fld._fname+"' is only partially set in the constructor",null); + } + } + return null; + } + + @Override public boolean eq( Node n ) { + return !_fun.isDead() && !((ReturnNode)n)._fun.isDead(); + } + + // ------------ + // MachNode specifics, shared across all CPUs + public String op() { + return _fun._frameAdjust > 0 ? "epilog" : "ret "; + } + // Correct Nodes outside the normal edges + public void postSelect(CodeGen code) { + _fun.setRet(this); + } + public RegMask regmap(int i) { + return i==2 + ? CodeGen.CODE._mach.retMask(_fun.sig()) + : CodeGen.CODE._retMasks[i]; + } + public RegMask outregmap() { return null; } + public void encoding( Encoding enc ) { throw Utils.TODO(); } + public void asm(CodeGen code, SB sb) { + int frameAdjust = fun()._frameAdjust; + if( frameAdjust>0 ) + sb.p("\n").p("rsp += #").p(frameAdjust).p("\nret "); + // Post code-gen, just print the "ret" + if( code._phase.ordinal() < CodeGen.Phase.RegAlloc.ordinal() || + (code._phase.ordinal() == CodeGen.Phase.RegAlloc.ordinal() && !code._regAlloc.done()) ) + // Prints return reg (either RAX or XMM0), RPC and then the + // callee-save registers. + for( int i=2; i<nIns(); i++ ) + sb.p(code.reg(in(i))).p(" "); + // If we did not get the expected rpc, print which one we got + else if( code._regAlloc.regnum(rpc()) != code._mach.rpc() ) + sb.p("[").p(code.reg(rpc())).p("]"); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/RoundF32Node.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/RoundF32Node.java new file mode 100644 index 00000000..02d3bbaa --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/RoundF32Node.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.node; + + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; + + +public class RoundF32Node extends Node { + public RoundF32Node(Node lhs) { super(null, lhs); } + + + @Override protected String format() { return "((f32)%1)"; } + + @Override + public Type compute() { + if (in(1)._type instanceof TypeFloat i0 && i0.isConstant() ) + return TypeFloat.constant((float)i0.value()); + if( in(1)._type==TypeFloat.F64 ) return TypeFloat.F32; + 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/chapter-modules/src/main/java/com/seaofnodes/simple/node/SarNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/SarNode.java new file mode 100644 index 00000000..fa63334b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/SarNode.java @@ -0,0 +1,37 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +public class SarNode extends ArithNode { + public SarNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + + + @Override public String op() { return ">>"; } + @Override long doOp( long x, long y ) { return x >> y; } + @Override TypeInteger doOp( TypeInteger x, TypeInteger y ) { + if( y.isConstant() ) { + int log = (int)y.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 && forceInt(lhs) && lhs._type instanceof TypeInteger ) + return lhs; + + // TODO: x >> 3 >> (y ? 1 : 2) ==> x >> (y ? 4 : 5) + + return super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new SarNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ScopeNode.java new file mode 100644 index 00000000..694d5dcb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -0,0 +1,719 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Var; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.CompUnit; +import com.seaofnodes.simple.codegen.ParseAll; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +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. + // Maps 1-to-1 to the _inputs array. + public final Ary<Var> _vars; + + // Lexical scope is typed: + public static class Kind { + // Basic block scoping + public static class Block extends Kind { } + // Constructor/initializer block for an allocated struct. + public static class Constructor extends Block { + public Constructor(TypeStruct self) { _self = self; } + public final TypeStruct _self; + } + // Function scope + public static class Func extends Kind { + public final String _name; + public final boolean _constructor; + public Func(String name) { this(name,false); } + public Func(String name, boolean constructor) { _name=name; _constructor=constructor; } + } + + public int _lexSize; // Number of Vars in this scope + } + public final Ary<Kind> _kinds; + // Lexical scope nesting depth + public int depth() { return _kinds._len; } + public Kind klast() { return _kinds.last(); } + + // Extra guards; tested predicates and cast results + private final Ary<Node> _guards; + + // A new ScopeNode + public ScopeNode() { + super(); + _vars = new Ary<>(Var .class); + _kinds = new Ary<>(Kind.class); + _guards = new Ary<>(Node.class); + } + + @Override public String label() { return "Scope"; } + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + p.p("Scope[ "); + int j=1; + for( int i=0; i<nIns(); i++ ) { + if( j < depth() && i == _kinds.at(j)._lexSize ) { p.p("| "); j++; } + Var v = var(i); + p.p(v._type()); + p.p(" "); + if( v._final ) p.p("!"); + p.p(v._name); + p.p("="); + Node n = in(i); + while( n instanceof ScopeNode loop ) { + p.p("Lazy_"); + n = loop.in(i); + } + p.n(n); + p.p(", "); + } + return p.unchar(", ").p("]"); + } + + + public Node ctrl() { return in(0); } + public Node mem() { return in(1); } + public Var var(int i) { return _vars.at(i); } + + /** + * 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 ) { + kind._lexSize = _vars.size(); + _kinds.push(kind); + } + + // Pop a lexical scope + public void pop() { + promote(null); // Promote forward references to the next outer scope + _pop(); + } + // Pop a scope, no promote FRefs + public void _pop() { + int n = _kinds.pop()._lexSize; + popUntil(n); // Pop off inputs going out of scope + _vars.setLen(n); // Pop off variables going out of scope + } + + + // 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(CodeGen code) { + Kind kind = klast(); + int n = kind._lexSize; + for( int i=n; i<nIns(); i++ ) { + Var v = var(i); + Node vn = in(i); + if( !v.isFRef() ) continue; + // Shuffle vars and inputs to move the forward reference to + // *before* the start of the using scope ("as if" it was not + // actually a forward reference). + for( int j=n; j<=i; j++ ) { + Var vtmp = var(j); + Node ntmp = in (j); + v._idx = j; + _vars .set(j,v ); + _inputs.set(j,vn); + v = vtmp; + vn = ntmp; + } + kind._lexSize = ++n; + } + } + + + public boolean inFunction () { return klast() instanceof Kind.Func; } + // Is any enclosing scope a constructor? + public boolean inConstructor() { return constructorSelf() != null; } + // Nearest constructor self type, if any. + public TypeStruct constructorSelf() { + for( int i=depth()-1; i>=0; i-- ) + if( _kinds.at(i) instanceof Kind.Constructor ctor ) + return ctor._self; + else if( _kinds.at(i) instanceof Kind.Func func && FunNode.isInstance(func._name) ) + return ((TypeMemPtr)var(_kinds.at(i)._lexSize).type())._obj; + return null; + } + + // Find nearest enclosing function scope + public int enclosingFunction() { + for( int i=depth()-1; i>=0; i-- ) + if( _kinds.at(i) instanceof Kind.Func func && !FunNode.isInstance(func._name) ) + return i; + throw Utils.TODO("Should not reach here"); + } + + // Find nearest enclosing function or declaration scope + public int enclosingFuncOrDecl() { + for( int i=depth()-1; i>=0; i-- ) + if( _kinds.at(i) instanceof Kind.Func func ) + return i; + throw Utils.TODO("Should not reach here"); + } + + // Which lexical scope contains this Var? + public int kindx( Var v ) { + for( int i=depth()-1; i>=0; i-- ) + if( v._idx >= _kinds.at(i)._lexSize ) + return i; + return -1; + } + + // Which lexical scope contains this Var? + + + public int kindFcnx( Var v ) { + for( int i=depth()-1; i>=0; i-- ) + if( v._idx >= _kinds.at(i)._lexSize && _kinds.at(i) instanceof Kind.Func ) + return i; + throw Utils.TODO("Should not reach here"); + } + + // Which function scope contains this Var? + public Kind kindFcn( Var v ) { + int kx = kindFcnx(v); + return kx == -1 ? null : _kinds.at(kx); + } + + // Final fields are writable local values inside their own initializer or + // constructor. Final parameters, local variables, and captured bindings + // remain final. Use the same rule when deciding which loop Phis to build. + public boolean canInit(Var v) { + int fx=enclosingFuncOrDecl(); + Kind.Func fun=(Kind.Func)_kinds.at(fx); + if( !fun._constructor || v._extern!=0 ) return false; + int dx=FunNode.isInstance(fun._name) ? fx : fx-1; + return dx>=0 && kindx(v)==dx && v._idx>=_kinds.at(dx)._lexSize+2; + } + + private boolean loopVar(Var v) { + return !v._final || (canInit(v) && !v.type().isHighOrConst()); + } + + // 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. + public int find( String name ) { + for( int i=_vars.size()-1; i>=0; i-- ) + if( var(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 _kinds.isEmpty() || name!=MEM0 ; // Later scopes do not define memory + if( depth() > 0 ) + for( int i=_vars.size()-1; i>=klast()._lexSize; i-- ) { + Var n = var(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 + return true; + } + } + Var v = new Var(nIns(),name,declaredType,xfinal,loc,init==CodeGen.CODE.XCTRL); + _vars.add(v); + // Creating a forward reference + if( init==CodeGen.CODE.XCTRL ) + //init = new FRefNode(v).init(); + throw Utils.TODO(); + addDef(init); + return true; + } + + // Normal lookup was tried and failed. Append a forward ref in the current + // lexical scope. Scope pops promote it outwards "as if" the final constant + // had been declared before the using scope. + + public Var defineFRef( String id, Type t, boolean xfinal, Parser.Lexer loc ) { + Var var = new Var(nIns(),id,t,xfinal,loc,true); + FRefNode fref = new FRefNode(id,loc).init(); + fref._type = fref._con = t; + _vars.add(var); + addDef(fref); + return var; + } + + + // Parser state carries one complete, conservative memory value. Precise + // alias partitions belong to the graph and are introduced by peepholes. + public Node mem( int alias ) { + assert alias == 1; + return in(update(var(1),null)); + } + public void mem( int alias, Node st ) { + assert alias == 1; + update(var(1),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(var(idx),null); + } + + /** + * Redefine an existing name + * + * @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(var(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,PhiNode.make(v._name, v.type(), loop.ctrl(), loop.in(loop.update(v,null)._idx),null).init()); + 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._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 is one ordinary parser-state value. At loop heads the + // Scope sentinel triggers the same lazy-Phi machinery as variables. + dup.addDef(loop ? this : mem()); + + // 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 && loopVar(var(i)) ? 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()); + this._merge(that,r); + that.kill(); // Kill merged scope + CodeGen.CODE.add(r); + return r.unkeep(); + } + + public void _merge(ScopeNode that, RegionNode r) { + _merge(that,r,nIns()); + } + public void _merge(ScopeNode that, RegionNode r, int max ) { + for( int i = 1; i < max; 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 = var(i); + Node lhs = this.in(this.update(v,null)); + Node rhs = that.in(that.update(v,null)); + setDef(i, PhiNode.make(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. + // 'this' scope is the default, 'scope' might have frefs. + public void balanceIf( ScopeNode scope ) { + if( nIns()==scope.nIns() ) + return; // Fast-path cutout + int kidx = enclosingFunction(); + assert _kinds.at(kidx) == scope._kinds.at(kidx); + if( kidx==0 ) kidx = 1; + for( int i = _kinds.at(kidx-1)._lexSize; i < scope.nIns(); i++ ) { + Var n = scope.var(i); + if( i<nIns() && var(i)==n ) continue; + if( n.isFRef() ) { // RHS has forward refs + _vars.insert(n,i); + insertDef(i,scope.in(i)); + } else + throw Parser.error("Cannot define a '"+n._name+"' on one arm of an if",n._loc); + } + } + + public void balanceLoopFRefs( ScopeNode scope ) { + for( int i = nIns(); i < scope.nIns(); i++ ) { + Var n = scope.var(i); + if( n.isFRef() ) { // Loop body has forward refs + Var v = new Var(nIns(),n._name,n.type(),n._final,n._loc,true); + v._uninit = n._uninit; + _vars.add(v); + addDef(scope.in(i)); + } + } + } + + + // ------------------------------------------------ + // 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()); + + this._endLoop( this, back, exit); + back.kill(); // Loop backedge is dead + // Now one-time do a useless-phi removal + this._useless(); + } + + // Fill in the backedge of any inserted Phis + void _endLoop( ScopeNode scope, Node back, Node exit ) { + for( int i=1; i<nIns(); i++ ) { + if( !loopVar(var(i)) ) continue; + Node n = in(i); + if( var(i).type().isHighOrConst() && !(n instanceof PhiNode phi && phi.inProgress()) ) { // Cannot lift higher than a constant, so no Phi + Node con = ConstantNode.make(var(i).type()).init(); + n.subsume(con); + n = con; + } else if( back.in(i) != scope ) { + PhiNode phi = (PhiNode)n; + 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,n); + } + } + + // ------------------------------------------------ + // 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. + // + // This operation is lexical, not a full semantic theorem prover, and thus + // its behavior changes the language spec. Optimizations that hide the + // lexical structure (e.g. constant folding control flow) can hide what is + // guarded or how the guard is applied, making it somewhat fragile. Several + // good use cases are covered here, but not everything you might like. + // + // Simple lexical facts work well: + // + // ``` + // if( ptr ) + // return ptr.field; // `ptr` is guarded non-null here. + // + // if( a && a.ptr ) + // return a.ptr.x; // `a` is guarded while parsing the RHS load, + // // and remains guarded on the true arm. + // ``` + // + // Short-circuit expressions are lowered as nested Phis. We recursively + // decompose the recognized Phi shape, so chains such as: + // + // ``` + // if( x && y && z ) + // use(x,y,z); + // ``` + // + // discover the nested `x && y` and then `x`, `y`, and `z`; this is not + // merely a one-level lookup. Likewise, nested tests carry lexical guards + // inwards: + // + // ``` + // if( x && y ) + // if( z && a ) + // if( x.ptr && z.ptr ) + // ... + // ``` + // + // Here `x` is already guarded while parsing the outer true arm, `z` while + // parsing the inner true arm, and both are available before the matching + // loads. The later post-test decomposition is just a convenience for + // values still schedulable at the post-test control. + // + // The hard limit is that a semantic implication is not always a + // schedulable Node. For `(x && y)`, the true side implies that `y` was + // true, but `y` might be a value computed only on the RHS control arm: + // + // ``` + // val ptr = ...; + // if( ptr && (ptr=fcn() ? 0 : arg ) {} + // ``` + // + // A Guard pinned after the Phi merge and using the branch-local `ptr` + // value would give that value a use above its defining arm. GCM then has + // no legal block between the value's early control and the LCA of its + // uses. Thus `_addGuards` first checks that the predicate's effective + // early CFG dominates the control where the Guard would be pinned. If it + // does not, we skip that guard even if the fact is semantically true. + // + // A more complete answer would need a richer logical/proof node, or would + // need to rematerialize predicates on the guarded path instead of using + // branch-local nodes directly. Plain GuardNodes are intentionally kept to + // facts represented by values already available at the guard control. + // + // TODO: Make this more robust for shapes like: + // ``` + // if( !ary || !ary# ) exit(); // facts about ary are lost, despite the early exit + // for( ...; ary# ) ...; + // ``` + // Here the guard ends up guarding the whole expression (both ary and ary# + // are not-null), but loses that ary is not-null. A better answer might be + // a dedicated logical expression node, or some sort of gather-of-checked + // values to Guard post-test. + + public void addGuards( CFGNode ctrl, Node pred, boolean invert ) { + _guards.add(ctrl); // Marker between guard sets + // add pred & its cast to the normal input list, with special Vars + if( pred==null || pred.isDead() ) + return; // Dead, do not add any guards + _addGuards(ctrl,pred,invert); + } + + private void _addGuards( CFGNode ctrl, Node pred, boolean invert ) { + if( pred==null || pred.isDead() ) + return; + CFGNode early = CFGNode.earlyCFG(pred); + if( early != null && !early.dominates(ctrl,null) ) + return; + // A single negation is handled below. For !!p (including p != null) + // or a negated short-circuit Phi, also discover the underlying facts. + if( pred instanceof NotNode not && + (not.in(1) instanceof NotNode || not.in(1) instanceof PhiNode) ) { + pred.keep(); // Recursive peepholes may rediscover this Not. + _addGuards(ctrl,not.in(1),!invert); + pred.unkeep(); + } + // Short-circuit logic is represented by a Phi. For `a || b` being + // false, or `a && b` being true, both individual operands have the + // same proven value as the whole expression. The "skipped RHS" path + // is the Region input whose Phi value is the controlling If predicate. + if( pred instanceof PhiNode phi && phi.nIns()==3 && phi.region() instanceof RegionNode r && r.nIns()==3 ) + for( int i=1; i<3; i++ ) + if( r.in(i) instanceof CProjNode prj && prj.ctrl() instanceof IfNode iff ) { + Node skip = shortCircuitSkippedPred(phi.in(i),iff.pred(),prj,invert); + if( skip != null ) { + _addGuards(ctrl,skip,invert); + _addGuards(ctrl,phi.in(3-i),invert); + break; + } + } + // Invert the If conditional + if( invert ) + pred = pred instanceof NotNode not ? not.in(1) : CodeGen.CODE.add(new NotNode(pred).peephole()); + Node zeroPred = pred instanceof NotNode not ? not.in(1) : null; + // This is a zero/null test. + // Compute the positive test type. + _addGuard(true,ctrl,pred); + + // Compute the negative test type. + if( zeroPred != null ) + _addGuard(false,ctrl,zeroPred); + } + + private static Node shortCircuitSkippedPred( Node val, Node pred, CProjNode prj, boolean invert ) { + if( val == null ) + return null; + if( val == pred ) + return (prj._idx==0)==invert ? val : null; + if( val instanceof NotNode not && not.in(1)==pred ) + return (prj._idx==1)==invert ? val : null; + if( val._type == TypeInteger.TRUE && invert ) + return prj._idx==0 ? pred : CodeGen.CODE.add(new NotNode(pred).peephole()); + if( val._type == TypeInteger.FALSE && !invert ) + return prj._idx==1 ? pred : CodeGen.CODE.add(new NotNode(pred).peephole()); + return null; + } + + private void _addGuard(boolean nonZero, Node ctrl, Node pred) { + Node guard = new GuardNode(nonZero,ctrl,pred).peephole(); + if( guard != pred ) { + pred.keep(); + guard.keep(); + _guards.add(pred); + _guards.add(guard); + replace(pred,guard,nonZero); + } + } + + + // Remove matching pred/cast pairs from this guarded region. + public void removeGuards( Node ctrl ) { + assert ctrl instanceof CFGNode; + // Pop the guards up to this region's marker. + 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 + } + } + + // 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, boolean nonZero ) { + assert old!=null && old!=cast; + for( int i=0; i<nIns(); i++ ) + if( in(i)==old || nonZero && nullCheckedPhi(in(i),old) ) + setDef(i,cast); + } + + private static boolean nullCheckedPhi( Node n, Node old ) { + if( !(n instanceof PhiNode phi) || phi.nIns()!=3 ) + return false; + if( phi.in(1)==null || phi.in(2)==null ) + return false; + int zero = phi.in(1)._type == phi.in(1)._type.makeZero() ? 1 : + phi.in(2)._type == phi.in(2)._type.makeZero() ? 2 : -1; + if( zero == -1 ) + return false; + Node val = phi.in(3-zero); + return val==old || + val instanceof GuardNode guard && guard._nonZero && guard.in(1)==old; + } + + // ------------------------------------------------------------------------ + // Insert a top-level public external symbol, "as-if" some #include/import + // was executed at the top of tile. + public Var insertExtern( ExternNode ext ) { + // Size of top-level scope + int lexSize = _kinds.at(0)._lexSize; + Var var = new Var(lexSize,ext._extern,ext._con,true,null); + insert(var,ext,0); + return var; + } + + // ------------------------------------------------------------------------ + // Insert a Var with Node n in the lex scope just prior to kidx + private void insert( Var var, Node n, int kidx ) { + // Insert just prior to scope kidx + int nidx = _kinds.at(kidx)._lexSize; + _vars.insert(var,nidx); + insertDef(nidx,n); + // All outer Kinds lexical offsets bump by one + for( int i = kidx; i < depth(); i++ ) + _kinds.at(i)._lexSize++; + // All Vars outside the innermost lexical scope bump out one + for( int i=nidx+1; i<_vars._len; i++ ) + var(i)._idx++; + } + + // Save the existing Node state to an array + public Node[] save() { + Node[] olds = new Node[nIns()]; + for( int i=0; i<nIns(); i++ ) + olds[i] = in(i)==null ? null : in(i).keep(); + return olds; + } + + // Restore saved Node state back into scope + public void restore( Node[] olds ) { + assert olds.length<=nIns(); + for( int i=0; i<olds.length; i++ ) + setDef(i,olds[i]==null ? null : olds[i].unkeep()); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ShlNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ShlNode.java new file mode 100644 index 00000000..380fc246 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ShlNode.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +public class ShlNode extends ArithNode { + public ShlNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + + + @Override public String op() { return "<<"; } + @Override long doOp( long x, long y ) { return x << y; } + @Override TypeInteger doOp( TypeInteger x, TypeInteger y ) { + if( x == TypeInteger.ZERO ) + return TypeInteger.ZERO; + if( y.isConstant() ) { + int shf = (int)y.value(); + // If no overflow, shift endpoints + if( !(((x._min<<shf)>>shf) != x._min || + ((x._max<<shf)>>shf) != x._max ) ) + return TypeInteger.make(x._min<<shf,x._max<<shf); + } + 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 && forceInt(lhs) && lhs._type instanceof TypeInteger ) + return lhs; + // (x + c) << i => (x << i) + (c << i) + if( lhs instanceof AddNode add && addDep(add).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(), CodeGen.CODE.con(sum) ); + } + } + + // TODO: x << 3 << (y ? 1 : 2) ==> x << (y ? 4 : 5) + + return super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new ShlNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ShrNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ShrNode.java new file mode 100644 index 00000000..17170a1f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ShrNode.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +public class ShrNode extends ArithNode { + public ShrNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + + + @Override public String op() { return ">>>"; } + @Override long doOp( long x, long y ) { return x >>> y; } + @Override TypeInteger doOp( TypeInteger x, TypeInteger y ) { + return x == TypeInteger.ZERO ? x : 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 && forceInt(lhs) && lhs._type instanceof TypeInteger ) + return lhs; + + // TODO: x >>> 3 >>> (y ? 1 : 2) ==> x >>> (y ? 4 : 5) + + return super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new ShrNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/SplitNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/SplitNode.java new file mode 100644 index 00000000..3042f857 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/SplitNode.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.LRG; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; + +public abstract class SplitNode extends MachConcreteNode { + public final LRG _lrg; // Live range for split + public final String _kind; // Kind of split + public final byte _round; // Debugging info for which RegAlloc round + public SplitNode(LRG lrg,String kind, byte round, Node[] nodes) { super(nodes); _lrg=lrg; _kind = kind; _round = round; } + // A register-only transfer may need another split to reach stack storage. + public boolean isNonStackSplit() { return false; } + @Override public String op() { return "mov"; } + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + p.p("mov("); + if( in(1) == null ) p.p("---"); + else p.n(in(1)); + return p.close(); + } + public FunNode fun(Encoding enc) { return enc._code._regAlloc.lrg(this)._fun; } + @Override public Type compute() { return in(0)._type; } + @Override public Node idealize() { return null; } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))); + } + @Override public String comment() { return _kind + " #"+ _round; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartCUNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartCUNode.java new file mode 100644 index 00000000..0f7a322a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartCUNode.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// Start of a single compilation unit, distinct from the outer whole-program Start. +public class StartCUNode extends StartNode { + public final String _fname; + public StartCUNode(StartNode start, StopCUNode stop, Type arg, String fname) { super(start,stop,arg); _fname = fname; } + public StartCUNode(StartCUNode start) { super(start); _fname = start._fname; } + + + @Override public CFGNode cfg0() { return (CFGNode)in(0); } + + // IDom is cfg0() and depth is always Start+1 which is 0+1 or 1 + @Override public int idepth() { return 1; } + @Override public CFGNode idom(Node dep) { return cfg0(); } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartNode.java new file mode 100644 index 00000000..3c605e30 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StartNode.java @@ -0,0 +1,87 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; +import java.util.Arrays; +import java.util.HashSet; + +/** + * The Start node represents the external world calling, be it calling the + * entry <clinit> or just some linked caller calling. + * <p> + * 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. + * <p> + * By ch25, Start is the external world (or linker) calling with everything + * that has escaped. Start is NOT the entry <clinit> itself, which can assume + * no memory nor fidxs exist nor escaped. Start gets escaped things from Stop; + * together these close the Grand Cycle between the code we know and the code + * we don't. + */ +public class StartNode extends LoopNode implements MultiNode { + + final Type _arg; + + public StartNode(StartNode start, StopNode stop, Type arg) { super(null,start,stop); _arg = arg; } + public StartNode(StartNode start) { super(start); _arg = start==null ? null : start._arg; } + + public Type arg() { return _arg; } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.p(label()); + } + + @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, it's the class init for + // this compilation unit. + @Override public CFGNode uctrl() { + // Find module.<clinit>, it's the start. + CFGNode C = null; + for( Node use : _outputs ) + if( use instanceof FunNode fun && CodeGen.CODE.entryClinitName().equals(fun._name) ) + { assert C==null; C = fun; } + if( C==null ) + for( FunNode fun : CodeGen.CODE._linker ) + if( fun!=null && !fun.isDead() && CodeGen.CODE.entryClinitName().equals(fun._name) ) + { assert C==null; C = fun; } + return C; + } + + boolean escapedFIDX(int fidx) { + // Public class fields are callable by the unknown external world + // regardless of the current SCCP approximation of Start memory. + if( CodeGen.CODE.publicFIDX(fidx) ) return true; + if( !(_type instanceof TypeTuple tt) ) + return !_type.isHigh(); // TOP never escapes, BOTTOM always escapes + TypeMem tmem = (TypeMem)tt._types[1]; + if( tmem == TypeMem.BOT ) return false; + if( tmem == TypeMem.TOP ) return false; + if( XInt.isHigh(tmem._escFs) ) + return false; + return XInt.bit(tmem._escFs,fidx); + } + + @Override public TypeTuple compute() { + StopNode stop = (StopNode)in(1); + TypeMem tmem = stop._type instanceof TypeTuple tt + ? tt.mem() + : stop._type.isHigh() ? TypeMem.START : TypeMem.BOT; + assert tmem._alias==1 && !tmem._one; + return TypeTuple.make(Type.CONTROL,tmem,_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; } + + @Override public boolean eq( Node n ) { return true; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/StopCUNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StopCUNode.java new file mode 100644 index 00000000..53c2325c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StopCUNode.java @@ -0,0 +1,126 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// Stop of a single compilation unit, distinct from the outer whole-program Stop. +public class StopCUNode extends StopNode { + public StopCUNode() { super(); } + public StopCUNode(StopNode stop) { super(stop); } + + + public StartCUNode start() { + for( Node use : _outputs ) + if( use instanceof StartCUNode start ) + return start; + return null; + } + + @Override + public TypeTuple compute() { + // During Parsing, new Stops and Returns are being added. This Stop + // cannot know if more are coming, so must assume the worst. + if( CodeGen.CODE._phase ==null || CodeGen.CODE._phase == CodeGen.Phase.Parse ) + return TypeTuple.STATE; + + StartCUNode start = start(); + // Memory start from all returns + TypeMem xmem = TypeMem.START; + // Escape summaries are monotone discovery facts. In Opto, a Return + // can widen from a precise set to FULL; the public-only normalization + // below must not forget private bits already discovered precisely from + // that Return on an earlier SCCP step. + if( CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Opto.ordinal() && + CodeGen.CODE._phase.ordinal() <= CodeGen.Phase.Lower.ordinal() && + _type instanceof TypeTuple old && old.mem() instanceof TypeMem omem ) + xmem = xmem.makeFrom(XInt.meet(xmem._escFs,omem._escFs), + XInt.meet(xmem._escAs,omem._escAs)); + + for( Node def : _inputs ) { + ReturnNode ret = (ReturnNode)def; + if( ret._type == Type.TOP ) continue; + if( !(ret._type instanceof TypeTuple tret) ) + return TypeTuple.STATE; // Must be Type.BOTTOM + + FunNode fun = ret.fun(); + boolean noUserCtor = fun.isInstance() && + !CodeGen.hasUserConstructor(((TypeMemPtr)fun.sig().arg(0))._obj); + // Instance <init> is an allocation helper, never an externally + // callable constructor when a user constructor exists. Otherwise + // the same no-arg <init> is the public constructor. + boolean pub = fun.isPublic() && (fun.isClz() || noUserCtor); + TypeFunPtr sig = fun.sig(); + // If public init, the function escapes and the related class escapes + if( pub ) { + xmem = xmem.escapes(sig); + // Classes also escape their public clazz pointer, which then + // escapes all their public fields. Private fields do not + // escape, and are only available from the local class code. + if( fun.isClz() ) + xmem = xmem.escapesAliases(sig._sig[0]); + } + + // The instance <init> Start edge supplies conservative pre-Opto + // inputs, but its private-memory result is consumed only by the + // allocation sequence and is never an externally visible return. + if( fun.isInstance() && !noUserCtor ) continue; + + // Does this function escape? If not, then no need to MEET + // here as its effects are not visible to the outside world. + addDepForwards(start); // Check again if function escapes + if( pub || start.escapedFIDX(sig.fidx()) ) { + // External callers already hold their object arguments, so + // writes through those arguments expose addresses too. + for( Node use : fun._outputs ) + if( use instanceof ParmNode p && p._idx>=2 ) + xmem=publicBottom(xmem.escapes(addDepForwards(p)._type)); + if( tret.ctl().isHigh() ) continue; // Return not reachable + // Capture (precisely?) all escaping pointer aliases and fidxs. + // Escaped fidxs means the linked world can call that function; + // escaped aliases means the linked world can R/W those aliases. + xmem = xmem.escapes(tret.ret()); + xmem = publicBottom(xmem); + xmem = (TypeMem)xmem.meet(publicBottom(tret.mem(),xmem)); + } + } + // Native retention is shared by the whole program, including calls + // on paths with no Return. A class boundary must not hide it. + for( CallNode call : CodeGen.CODE._calls ) + if( !call.isDead() ) xmem=publicBottom(xmem.escapes(call.exposed(this))); + assert xmem._alias==1 && !xmem._one; + return TypeTuple.make(Type.CONTROL,xmem,Type.BOTTOM); + } + + // FULL escape sets arise from global BOTTOM. Across a compilation-unit + // boundary global BOTTOM exposes every public field, while retaining any + // private escapes already discovered precisely. + private TypeMem publicBottom(TypeMem mem) { return publicBottom(mem,mem); } + private TypeMem publicBottom(TypeMem mem, TypeMem prior) { + // The pessimistic Iter pass naturally lifts transient FULL inputs to + // precise finite sets. Replacing FULL early there would let a later + // input refinement add bits, reversing Iter's monotonic direction. + // This normalization is needed only for Opto's optimistic grand cycle. + if( CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal() || + CodeGen.CODE._phase.ordinal() > CodeGen.Phase.Lower.ordinal() ) return mem; + int[] escFs = XInt.isHigh(mem._escFs) + ? XInt.meet(prior._escFs,CodeGen.CODE.publicFIDXs()) + : mem._escFs; + int[] escAs = XInt.isHigh(mem._escAs) + ? XInt.meet(prior._escAs,CodeGen.CODE.publicAliases()) + : mem._escAs; + return escFs==mem._escFs && escAs==mem._escAs ? mem : mem.makeFrom(escFs,escAs); + } + + @Override + public Node idealize() { + int len = nIns(); + for( int i=0; i<nIns(); i++ ) + if( in(i) instanceof ReturnNode ret && addDep(ret.fun()).isDead() ) + delDef(i--); + if( len != nIns() ) return this; + return null; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StopNode.java new file mode 100644 index 00000000..7238f1f9 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -0,0 +1,94 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// A collection of function ReturnNodes for a compilation unit. +public class StopNode extends CFGNode { + + public StopNode() { + super(); + _type = compute(); + } + public StopNode(StopNode stop) { super(stop); } + + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + // For the sake of many old tests, and single value prints as "return val" + ReturnNode ret1 = null; + for( Node n : _inputs ) { + if( n instanceof ReturnNode ret ) { + if( ret1 != null ) { ret1=null; break; } + ret1 = ret; + } + } + if( ret1!=null ) return p.n(ret1); + + p.p("Stop[ "); + for( Node n : _inputs ) + if( n instanceof ReturnNode ret ) { + String name = ret.fun()._name; + if( name== null || !name.startsWith("sys.") ) + p.n(ret).p(" "); + } + return p.p("]"); + } + + @Override public boolean blockHead() { return true; } + + + // If a single Return, return it. + // Otherwise, null because ambiguous. + public ReturnNode ret() { + ReturnNode ret1 = null; + for( Node n : _inputs ) { + // TODO: remove isPublic + if( n instanceof ReturnNode ret && ret.fun().isPublic() ) { + if( ret1 != null ) return null; // Ambiguous + ret1 = ret; + } + } + return ret1; + } + + @Override + public TypeTuple compute() { + // During Parsing, new Stops and Returns are being added. This Stop + // cannot know if more are coming, so must assume the worst. + if( CodeGen.CODE._phase ==null || CodeGen.CODE._phase == CodeGen.Phase.Parse ) + return TypeTuple.STATE; + + // Just meet-over-inputs. + TypeTuple tt = TypeTuple.STOP_HIGH; + for( Node def : _inputs ) + tt = (TypeTuple)tt.meet(def._type); + assert tt.mem()._alias==1 && !tt.mem()._one; + return tt; + } + + @Override public Node idealize() { + int len = nIns(); + for( int i=0; i<nIns(); i++ ) + if( in(i).nIns()==0 ) + delDef(i--); + if( len != nIns() ) return this; + return null; + } + + @Override public int idepth() { + if( validIDepth() ) return _idepth; + int d=0; + for( Node ret : _inputs ) + // All public results are hooked on the Stop, including data + // values, so must check CFG + if( ret instanceof CFGNode cfg ) + d = Math.max(d,cfg.idepth()+1); + return cacheIDepth(d); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StoreNode.java new file mode 100644 index 00000000..bdd07c8d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/StoreNode.java @@ -0,0 +1,326 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + * 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 { + + final boolean _init; // Initializing writes are allowed to write null + // Semantic store width in bytes. Zero is the sole undecided state; + // otherwise hardware and ideal Stores are the 1/2/4/8-byte variants. + private byte _size; + + /** + * @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 decl, Node ctrl, Node mem, Node ptr, Node off, Node value, boolean init) { + this(loc,name,alias,decl,ctrl,mem,ptr,off,value,init,(byte)0); + } + private StoreNode(Parser.Lexer loc, String name, int alias, Type decl, Node ctrl, Node mem, Node ptr, Node off, Node value, boolean init, byte size) { + super(loc, name, alias, false, decl, ctrl, mem, ptr, off, value); + _init = init; + _size = size; + } + + + @Override boolean canDrop(MemOpNode other, Node dep) { + return super.canDrop(other,dep) && _init==((StoreNode)other)._init; + } + + // Debugger label + @Override public String label() { return "st_"+mlabel(); } + @Override public boolean isMem() { return true; } + + public Node nnptr() { + if( !(ptr() instanceof GuardNode cast) ) return ptr(); + assert cast._nonZero; + return cast.in(1); + } + public Node val() { return in(4); } + public int storeSize() { assert _size==1 || _size==2 || _size==4 || _size==8; return _size; } + public int log_size() { return Integer.numberOfTrailingZeros(storeSize()); }; + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.p(".").p(_name).p("=").p( val()).p(";"); + } + + @Override + public Type compute() { + Type val = val()._type; + Type mem0 = mem()._type; + Type ptr0 = ptr()._type; + // Validate argument types + if( ptr0.isHigh() ) + return TypeMem.TOP; + if( !(mem0 instanceof TypeMem mem) || + !(ptr0 instanceof TypeMemPtr ptr) ) + return mem0.isHigh() || ptr0==Type.NIL ? TypeMem.TOP : TypeMem.BOT; + + // Sharpen memory value; required for narrowing stores where the parser + // inserts zero/sign masking and somebody reads the TypeMem type. + Type decl = declaredType(); + if( decl != Type.BOTTOM ) + { assert decl != Type.TOP; val = val.join(decl); } + // A singleton class's final constant field lives in the static image. + // Its precise memory slice is a constant too; ordinary peepholes can + // replace this Store before scheduling or instruction selection. + // A constant runtime value alone is not enough: mutable fields (notably + // $init) still need their writes, and instance fields need allocation. + Field fld = ptr._obj.field(_name); + if( _alias!=1 && ptr._one && Parser.startsClzPrefix(ptr._obj._name) && + fld!=null && fld._final && decl.isConstant() ) + return TypeMem.make(_alias,val,true,true,true,mem._escFs,mem._escAs).escapesFrom(val); + + // Allocation uses a private TypeMem and nothing else does. This + // memory is truly private; a temporary singleton until it escapes - + // which is never does in a constructor. + if( !_name.equals("[]") && mem._one ) + // Just track the stored value + return TypeMem.make(_alias,val,true,false,_init,null,null).escapesFrom(val); + + // Normal aliasing Store. + assert mem._alias==1 || mem._alias==_alias; // Perfect aliasing + + // Single alias store. If memory is fat, attempt to get a narrow slice. + Type tfld = mem._alias==1 && mem._t instanceof TypeStruct ts + ? ts._fields[ts.findAlias(_alias)]._t + : mem._t; + + // Same alias, meet into other fields. + Type t = val.meet(tfld); + // Bulk memory's final bit describes its conservative remainder, not + // this newly precise slice. Slice finality comes from the field. + boolean xfinal = fld != null && fld._final; + return TypeMem.make(_alias,t,mem._one,mem._clz,xfinal,mem._escFs,mem._escAs).escapesFrom(val); + } + + @Override + public Node idealize() { + // Freeze the semantic Store variant as soon as the pointer exposes a + // canonical field declaration. Waiting until after structural memory + // peeps can lose the declaration to a flow-sensitive constant field + // state. + if( _size==0 && (_size= storageType()) != 0 ) { + unlock(); // Width participates in the cached hash. + return init(); + } + + assert !(mem()._type instanceof TypeMem mem) || _alias == 1 || mem._alias == 1 || _alias == mem._alias; + + // Simple store-after-MemMerge to a known alias can bypass. Happens when inlining. + Node mem = mem(); + if( _alias != 1 && mem instanceof MemMergeNode merge ) { + setDef(1,merge.alias(_alias)); + return this; + } + + Field fld; + if( _alias==1 && ptr()._type instanceof TypeMemPtr tmp && (fld=tmp._obj.field(_name)) != null && _size != 0 ) { + // Expand Store(bulkMem,alias#1) into MemMerge(bulkMem,#N:Store(bulkMem,alias#N)). + + // Normally I'd like to just drop in this xform and move on, but + // users of the bulk Store memory might nontheless depend on the + // sharp memory type (e.g. I64) from the store's memory. Using the + // MemMerge means using the bulk memory which e.g. might be BOT. + // Check for store users with the same alias, and use the new store + // directly, preserving the sharper graph. + Node st = new StoreNode(_loc,_name,fld._alias,fld._t,in(0),mem,ptr(),off(),val(),_init,_size).peephole(); + for( int i=0; i<nOuts(); i++ ) { + Node use = out(i); + if( useAlias(use)==fld._alias ) { + int idx = use._inputs.find(this); + assert idx != -1; + CodeGen.CODE.add(use).setDef(idx,st); + i--; // setDef removed use from this Store's outputs + } + } + MemMergeNode mmm = mem instanceof BulkMemPhiNode bulk + ? bulk.aggregate(mem,fld._alias,st) + : new MemMergeNode(null,mem); + if( !(mem instanceof BulkMemPhiNode) ) + mmm.alias(fld._alias,st); + return mmm.init(); + } + + // 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==st._name; // Equiv class aliasing is perfect + setDef(1,st.mem()); + return this; + } + + // Private constructor memory describes the value that the next helper + // reads. Keep its conversion, even if a machine store could truncate it. + // This is the same rule as in the construction and escape chapters. + boolean priv = mem()._type instanceof TypeMem tm && tm._one; + // Value is automatically truncated by narrow store + if( !priv && _size!=0 && val() instanceof AndNode and && and.in(2)._type.isConstant() ) { + int log = 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; } + } + } + + // Store will chop high order bits off; math to change those bits can be dropped. + if( !priv && _size!=0 && val() instanceof SarNode shr && + shr.in(1) instanceof ShlNode shl && + shr.in(2)._type.isConstant() && + shl.in(2)._type.isConstant() ) { + TypeInteger shrC = (TypeInteger) shr.in(2)._type; + + // size of the thing that sign-extends + int base_size = (1 << shr.in(1)._type.log_size()) << 3; + int not_affected_bits = base_size - (int) shrC.value(); + int store_size = (1 << log_size()) << 3; + // if the store is unrelated to the shift amount, then get rid of the shift + if( shl.in(2)._type == shr.in(2)._type && shrC.value() >= store_size && not_affected_bits >= store_size) { + setDef(4, shl.in(1)); + return this; + } + } + + return null; + } + + // Flow-sensitive pointer types can carry a current field value (e.g. the + // constant zero in a u32 field). Storage width comes from the canonical + // struct declaration. If the pointer/definition is not available yet, + // leave the Store undecided instead of consulting the value or `_con`. + private byte storageType() { + if( !(ptr()._type instanceof TypeMemPtr tmp) ) + return 0; + TypeStruct obj = (TypeStruct)Parser.TYPES.get(tmp._obj._name); + Field fld = obj.field(_name); + if( fld == null || fld._t == null ) + return 0; + if( fld._t == Type.BOTTOM ) + return 0; // Store into a final field + assert fld._t != Type.TOP; + // Freshly parsed field stores retain the declaring structure on their + // symbolic offset even after the pointer's flow type sharpens. + if( fld._t.isConstant() ) { + if( !(off() instanceof ConFldOffNode coff) ) + return 0; + fld = coff._ts.field(_name); + if( fld == null || fld._t == null ) + return 0; + } + // Compute storage size from field type + byte size = (byte)(1 << fld._t.log_size()); + assert size==1 || size==2 || size==4 || size==8; + return size; + } + + // Alias required by a direct consumer of this Store's memory result. + // Keep in sync with BulkMemPhiNode.outputAlias. + private int useAlias(Node use) { + return switch( use ) { + case MemOpNode mem -> mem._alias; + case MemPhiNode phi -> phi._alias; + case EscapeNode esc -> esc.pub()==this ? esc.fld()._alias : 0; + default -> 0; + }; + } + + // Phi(mem, Store(phi,ptr,off,val)) -> Store(mem,ptr,off,Phi(load(mem),val)). + // With no observers of the backedge store, only the last value matters. + // The entry load preserves the field even when the loop takes zero trips. + Node sink(MemPhiNode phi) { + phi.addDepForwards(this); + if( nOuts()!=1 || _alias==1 || _size==0 || _init ) return null; + phi.addDep(ptr()); + // Start with a fixed field of an allocation dominating the loop. + // In particular, do not sink stores through a loop-varying address. + if( !(ptr() instanceof ProjNode p) || !(p.in(0) instanceof NewNode obj) || + !(ptr()._type instanceof TypeMemPtr tmp) || tmp._obj.isAry() || + !(off() instanceof ConstantNode) || err()!=null ) return null; + LoopNode loop = (LoopNode)phi.region(); + // Bulk splitting identifies precise slices by Region/alias. Finish it + // before replacing this memory point with a Store. + for( Node use : loop._outputs ) + if( use instanceof BulkMemPhiNode ) { + phi.addDepForwards(use); + return null; + } + phi.addDep(obj); + CFGNode ctrl = loop.entry(); + while( ctrl!=null && ctrl!=obj.cfg0() ) { + phi.addDep(ctrl); + ctrl=ctrl.idom(phi); + } + if( ctrl==null ) return null; + + Node mem = phi.in(1); + Node init = new LoadNode(_loc,_name,_alias,null,mem,ptr(),off()).peephole(); + Node value = CodeGen.CODE.add(new PhiNode(_name,loop,init,val()).peephole()); + Node sink = new StoreNode(_loc,_name,_alias,_con,null,mem,ptr(),off(),value,_init,_size); + // Break the memory cycle, killing the old store. Keep the Phi alive + // even if that store was its last use, until our caller replaces it. + phi.keep(); + phi.setDef(2,mem); + phi.unkeep(); + return sink; + } + + // Check that "mem" has no uses except "this" + private boolean checkOnlyUse(Node mem) { + if( mem.nOuts()==1 ) return true; + // 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 ) + addDep(use); + return false; + } + + @Override + public Parser.ParseException err() { + Parser.ParseException err = super.err(); + if( err != null ) return err; + assert _size>0 || CodeGen.CODE._phase.ordinal() <= CodeGen.Phase.FinalizeIR.ordinal(); + if( ptr()._type == Type.TOP ) // Dead store? + return null; + TypeMemPtr tmp = (TypeMemPtr)ptr()._type; + Field f = tmp._obj.field(_name); + if( f!=null && (tmp._ro || f._final) && !_init ) + return Parser.error("Cannot modify final field '"+_name+"'",_loc); + return null; + } + + // Do not GVN Stores. Two equal Stores on two of three paths are the easy + // failure: sharing them makes GCM place the Store on all three paths. + // Sharing is safe only if every exit path goes through an original Store, + // or GCM splits the shared Store to keep it on those paths. Stores must + // sink, not run on extra paths. GVN does not check this; GCM does not split. + // Identity also disables generic Phi factoring of Stores. + // The closed-loop Store sink is a separate peep and still applies. + @Override public boolean eq(Node n) { return this == n; } + @Override int hash() { return super.hash() ^ _size; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/SubNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/SubNode.java new file mode 100644 index 00000000..adad2ed5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/SubNode.java @@ -0,0 +1,56 @@ +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 ArithNode { + @Override boolean allowFloat() { return true; } + public SubNode(Node lhs, Node rhs) { super(null, lhs, rhs); } + public SubNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs,mode); } + + + @Override public String op() { return "-"; } + + @Override long doOp( long x, long y ) { return x - y; } + @Override double doOp(double x, double y ) { return x - y; } + @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { + // Sub of same is 0 + if( in(1)==in(2) ) + return TypeInteger.ZERO; + // Fold ranges like {2-3} - {0-1} into {1-3}. + if( !AddNode.overflow(x._min,-y._max) && + !AddNode.overflow(x._max,-y._min) && + y._min != Long.MIN_VALUE ) + return TypeInteger.make(x._min-y._max,x._max-y._min); + + return TypeInteger.BOT; + } + + @Override + public Node idealize() { + Node lhs = in(1); + Node rhs = in(2); + Type t1 = lhs._type; + Type t2 = rhs._type; + + // Keep invalid operands and unresolved numeric modes for type checking. + if( _mode==1 && t1 instanceof TypeInteger x && t2 instanceof TypeInteger y ) { + if( y.isConstant() && y.value()==0 ) return lhs; + if( x.isConstant() && x.value()==0 ) return new MinusNode(rhs); + } + + // x - (-y) is x+y + if( rhs instanceof MinusNode minus ) + return new AddNode(lhs,minus.in(1)); + + // (-x) - y is -(x+y) + if( lhs instanceof MinusNode minus ) + return new MinusNode(new AddNode(minus.in(1),rhs).peephole()); + + return super.idealize(); + } + + @Override Node copy(Node lhs, Node rhs) { return new SubNode(lhs,rhs,_mode); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java new file mode 100644 index 00000000..1acf55b6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java @@ -0,0 +1,26 @@ +package com.seaofnodes.simple.node; + + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.type.TypeInteger; + + +public class ToFloatNode extends Node { + public ToFloatNode(Node lhs) { super(null, lhs); } + + + @Override protected String format() { return "(flt)%1"; } + + @Override + public Type compute() { + if( in(1)._type.isHigh() ) return TypeFloat.F64.dual(); + if( in(1)._type == Type.NIL ) return TypeFloat.FZERO; + if( in(1)._type instanceof TypeInteger i0 && 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/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java new file mode 100644 index 00000000..9136aacc --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.type.TypeInteger; + +// Truncate toward zero, saturating overflow; NaN converts to zero. +public class ToIntegerNode extends Node { + public ToIntegerNode(Node lhs) { super(null,lhs); } + + @Override public String label() { return "ToInteger"; } + @Override protected String format() { return "(int)%1"; } + + @Override public Type compute() { + Type t = in(1)._type; + if( t.isHigh() ) return TypeInteger.TOP; + if( t instanceof TypeFloat tf && tf.isConstant() ) + return TypeInteger.constant((long)tf.value()); + return TypeInteger.BOT; + } + + @Override public Node idealize() { return null; } + @Override Node copy(Node lhs, Node rhs) { return new ToIntegerNode(lhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/TypeNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/TypeNode.java new file mode 100644 index 00000000..970472da --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/TypeNode.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; +import java.util.BitSet; +import java.util.HashMap; + +// Node carries an extra internal type +public abstract class TypeNode extends Node { + public Type _con; + public TypeNode(Type t, Node... nodes) { super(nodes); assert t!=null; _con = t; } + public TypeNode(TypeNode tn) { super(tn); _con = tn._con; } + // Copy edges from ideal, but 'this' is a Type-Mach-Node (e.g. MemOpX86) and needs a _con + public TypeNode(Node ideal, Type con) { super(ideal); _con = con; } + + // Upgrade the internal type + @Override boolean _upgradeType( HashMap<String,Type> TYPES) { + return liftType(_con.upgradeType(TYPES)); + } + + // Lift type + public boolean liftType( Type t ) { + if( t == _con ) return false; + unlock(); // Unlock before changing _con + _con = t; + return true; + } + + @Override public boolean eq(Node n) { return _con==((TypeNode)n)._con; } + @Override int hash() { return _con.hashCode(); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java new file mode 100644 index 00000000..3792e224 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; + +public class XCtrlNode extends CFGNode { + public XCtrlNode() { super(new Node[]{CodeGen.CODE._start}); } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p("Xctrl"); } + @Override public boolean isConst() { return true; } + @Override public Type compute() { return Type.XCONTROL; } + @Override public Node idealize() { return null; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/XorNode.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/XorNode.java new file mode 100644 index 00000000..e9cd13a5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/XorNode.java @@ -0,0 +1,43 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; + +public class XorNode extends ArithNode { + public XorNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + + + @Override public String op() { return "^"; } + @Override long doOp( long x, long y ) { return x ^ y; } + @Override TypeInteger doOp( TypeInteger x, TypeInteger y ) { + 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 && forceInt(lhs) && lhs._type instanceof TypeInteger ) + 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 super.idealize(); + } + @Override Node copy(Node lhs, Node rhs) { return new XorNode(_loc,lhs,rhs); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddARM.java new file mode 100644 index 00000000..76e47ca2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddARM.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AddARM extends MachConcreteNode implements MachNode { + AddARM( Node add) { super(add); } + @Override public String op() { return "add"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + // ADD (shifted register) + @Override public void encoding( Encoding enc ) { arm.r_reg(enc,this,Arm64.OP_ADD); } + // General form: "rd = rs1 + rs2" + @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))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddFARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddFARM.java new file mode 100644 index 00000000..67a10b48 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddFARM.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AddFARM extends MachConcreteNode implements MachNode { + AddFARM(Node addf) { super(addf); } + @Override public String op() { return "addf"; } + @Override public RegMask regmap(int i) { return arm.DMASK; } + @Override public RegMask outregmap() { return arm.DMASK; } + + //FADD (scalar) + @Override public void encoding( Encoding enc ) { arm.f_scalar(enc,this,Arm64.OPF_OP_ADD);} + + // Default on double precision for now(64 bits) + // General form: "addf rd = src1 + src2 + @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))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddIARM.java new file mode 100644 index 00000000..4c635d07 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AddIARM.java @@ -0,0 +1,28 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; + +public class AddIARM extends MachConcreteNode implements MachNode { + final int _imm; + AddIARM(Node add, int imm) { + super(add); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { + return _imm == 1 ? "inc" : (_imm == -1 ? "dec" : "addi"); + } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + //ADD (immediate) + @Override public void encoding( Encoding enc ) { + arm.imm_inst(enc,this, in(1), Arm64.OPI_ADD,_imm); + } + // General form: "addi rd = rs1 + imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" + #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndARM.java new file mode 100644 index 00000000..1a0365ad --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndARM.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +public class AndARM extends MachConcreteNode implements MachNode { + AndARM(Node and) { super(and); } + @Override public String op() { return "and"; } + @Override public String glabel() { return "&"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + // AND (shifted register) + @Override public void encoding( Encoding enc ) { arm.r_reg(enc,this,Arm64.OP_AND); } + // General form: #rd = rs1 & rs2 + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndIARM.java new file mode 100644 index 00000000..6292e98e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AndIARM.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AndIARM extends MachConcreteNode implements MachNode { + final int _imm; + AndIARM(Node and, int imm) { + super(and); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return "andi"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + arm.imm_inst_n(enc,this, in(1), Arm64.OPI_AND,_imm); + } + // General form: "andi rd = rs1 & imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" & #").p(Arm64.decodeImm12(_imm)); + } +}
\ No newline at end of file diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrARM.java new file mode 100644 index 00000000..77f14d4c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrARM.java @@ -0,0 +1,19 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Arithmetic Shift Right (register) +public class AsrARM extends MachConcreteNode implements MachNode { + AsrARM(Node asr) {super(asr);} + @Override public String op() { return "sar"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.DMASK; } + @Override public void encoding( Encoding enc ) { arm.shift_reg(enc,this,Arm64.OP_ASR); } + @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))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrIARM.java new file mode 100644 index 00000000..7cb42eec --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/AsrIARM.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +public class AsrIARM extends MachConcreteNode implements MachNode { + final int _imm; + AsrIARM(Node asr, int imm) { + super(asr); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return "asri"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + short rd = enc.reg(this); + short rn = enc.reg(in(1)); + assert _imm > 0; + enc.add4(Arm64.imm_shift(Arm64.OPI_ASR,_imm, 0b111111, rn, rd)); + } + // General form: "asri rd = rs1 >> imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" >> #").p(_imm); + } +}
\ No newline at end of file diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/BranchARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/BranchARM.java new file mode 100644 index 00000000..31bbde86 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/BranchARM.java @@ -0,0 +1,80 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Jump on flags, uses flags +public class BranchARM extends IfNode implements MachNode, RIPRelSize { + String _bop; + final boolean _fp; + BranchARM(IfNode iff, String bop, boolean fp ) { + super(iff); + _bop = bop; + _fp = fp; + } + @Override public String op() { return "j"+_bop; } + @Override public String label() { return op(); } + + @Override public void postSelect(CodeGen code) { + Node set = in(1); + if( set==null ) return; // Never-node cutout + Node cmp = set.in(1); + // Bypass an expected Set and just reference the cmp directly + if( set instanceof SetARM) + _inputs.set(1,cmp); + else + throw Utils.TODO(); + } + + @Override public RegMask regmap(int i) { assert i==1; return arm.FLAGS_MASK; } + @Override public RegMask outregmap() { return null; } + @Override public void negate() { _bop = negate(_bop); } + + @Override public void encoding( Encoding enc ) { + // Assuming that condition flags are already set. These flags are set + // by comparison (or sub). No need for regs because it uses flags + if( in(1)!=null ) { + // B.cond + enc.add4( Arm64.b_cond(Arm64.OP_BRANCH, 0, Arm64.make_condition(_bop, _fp)) ); + } else { + if( _bop=="!=" ) return; // Inverted, no code + enc.add4(Arm64.b(Arm64.OP_UJMP, 0)); + } + enc.jump(this,cproj(0)); + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { + if( in(1)==null && _bop=="!=" ) return 0; // Inverted never-node, no code + if( -(1<<19) <= delta && delta < (1<<19) ) return 4; + // 2 word encoding needs a tmp register, must teach RA + throw Utils.TODO(); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + assert !( in(1)==null && _bop=="!=" ); // Inverted never-node, no code no patch + if( opLen==4 ) { + enc.patch4(opStart,Arm64.b_cond(Arm64.OP_BRANCH, delta, Arm64.make_condition(_bop, _fp))); + } else { + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + if( in(1)!=null ) { // Never-node + String src = code.reg(in(1)); + if( src!="flags" ) sb.p(src).p(" "); + } else if( _bop=="!=" ) { + sb.p("never"); + return; + } + CFGNode prj = cproj(0).uctrlSkipEmpty(); + if( !prj.blockHead() ) prj = prj.cfg0(); + sb.p(label(prj)); + } + @Override public String comment() { return "L"+cproj(1)._nid; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallARM.java new file mode 100644 index 00000000..4de28f3a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallARM.java @@ -0,0 +1,50 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; + +public class CallARM extends CallNode implements MachNode, RIPRelSize { + final TypeFunPtr _tfp; + final String _name; + CallARM(CallNode call, TypeFunPtr tfp) { + super(call); + _inputs.pop(); // Pop constant target + assert tfp.isConstant(); + _tfp = tfp; + _name = CodeGen.CODE.funcName(tfp.fidx()); + } + + @Override public String op() { return "call"; } + @Override public String label() { return op(); } + @Override public String name() { return _name; } + @Override public TypeFunPtr tfp() { return _tfp; } + @Override public RegMask regmap(int i) { return arm.callInMask(_tfp,i,fun()._maxArgSlot); } + @Override public RegMask outregmap() { return null; } + @Override public int nargs() { return nIns()-2; } // Minus control, memory, fptr + + @Override public void encoding( Encoding enc ) { + FunNode fun = CodeGen.CODE.link(_tfp); + if( fun==null ) enc.external(this,_name); + else enc.relo(this); + // BL + enc.add4(Arm64.b(Arm64.OP_CALL,0)); // Target patched at link time + } + + // Delta is from opcode start, but X86 measures from the end of the 5-byte encoding + @Override public byte encSize(int delta) { return 4; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + enc.patch4(opStart,Arm64.b(Arm64.OP_CALL,delta)); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(_name).p(" "); + for( int i=0; i<nargs(); i++ ) + sb.p(code.reg(arg(i+2))).p(", "); + sb.unchar(2); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallRRARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallRRARM.java new file mode 100644 index 00000000..1949ae46 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallRRARM.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class CallRRARM extends CallNode implements MachNode { + CallRRARM(CallNode call) { super(call); } + @Override public String op() { return "callr"; } + @Override public String label() { return op(); } + @Override public RegMask regmap(int i) { + return i==_inputs._len + ? arm.RMASK // Function call target + : arm.callInMask(tfp(),i,fun()._maxArgSlot); // Normal argument + } + @Override public RegMask outregmap() { return null; } + + @Override public void encoding( Encoding enc ) { + // Needs a register, typically a jump-and-link-register opcode + // blr + short self = enc.reg(this); + enc.add4(Arm64.blr(Arm64.OP_CALLRARM, self)); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(fptr())).p(" "); + for( int i=0; i<nargs(); i++ ) + sb.p(code.reg(arg(i))).p(", "); + sb.unchar(2); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpARM.java new file mode 100644 index 00000000..1bd9c372 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpARM.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class CmpARM extends MachConcreteNode implements MachNode { + CmpARM( Node cmp ) { super(cmp); } + @Override public String op() { return "cmp"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.FLAGS_MASK; } + + // SUBS (shifted register) + @Override public void encoding( Encoding enc ) { arm.r_reg_subs(enc,this,Arm64.OP_CMP); } + + // General form: "cmp rs1, rs2" + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpFARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpFARM.java new file mode 100644 index 00000000..67ffc27b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpFARM.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// compare instruction on float regs input(D-registers) +public class CmpFARM extends MachConcreteNode implements MachNode{ + CmpFARM(Node cmp) {super(cmp);} + @Override public String op() { return "cmpf"; } + @Override public RegMask regmap(int i) { return arm.DMASK; } + @Override public RegMask outregmap() { return arm.FLAGS_MASK; } + + // FCMP + @Override public void encoding( Encoding enc ) { arm.f_cmp(enc,this); } + // General form: "cmp d0, d1" + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpIARM.java new file mode 100644 index 00000000..cd4dd9cf --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/CmpIARM.java @@ -0,0 +1,41 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Compare with immediate. +// CMP (immediate) - SUBS +public class CmpIARM extends MachConcreteNode implements MachNode { + final int _imm; + final String _bop; + CmpIARM(BoolNode bool, int imm) { + super(bool); + _inputs.pop(); + _bop = bool.op(); + _imm = imm; + } + CmpIARM( CmpIARM cmp ) { + super((Node)null); + addDef(null); + addDef(cmp.in(1)); + _bop = cmp._bop; + _imm = cmp._imm; + } + @Override public String op() { return _imm==0 ? "test" : "cmp"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.FLAGS_MASK; } + @Override public boolean isClone() { return true; } + @Override public Node copy() { return new CmpIARM(this); } + + @Override public void encoding( Encoding enc ) { arm.imm_inst_subs(enc,in(1),in(1), Arm64.OPI_CMP,_imm); } + + // General form: "cmp rs1, 1" + @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); + } + } diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/DivARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/DivARM.java new file mode 100644 index 00000000..e94b6491 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/DivARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class DivARM extends MachConcreteNode implements MachNode { + DivARM( Node div ) { super(div); } + @Override public String op() { return "div"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + // SDIV + @Override public void encoding( Encoding enc ) { arm.madd(enc,this,Arm64.OP_DIV,3); } + // 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))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/DivFARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/DivFARM.java new file mode 100644 index 00000000..68ad6047 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/DivFARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class DivFARM extends MachConcreteNode implements MachNode { + DivFARM( Node divf) { super(divf); } + @Override public String op() { return "divf"; } + @Override public RegMask regmap(int i) { return arm.DMASK; } + @Override public RegMask outregmap() { return arm.DMASK; } + + // FDIV (scalar) + @Override public void encoding( Encoding enc ) { arm.f_scalar(enc,this,Arm64.OPF_DIV); } + // General form: "VDIF = 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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java new file mode 100644 index 00000000..814a82c2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java @@ -0,0 +1,31 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +// Convert a narrow C return register to Simple's full-width integer value. +public class ExtendARM extends MachConcreteNode { + final int _bits; + final boolean _signed; + ExtendARM(ProjNode prj, TypeInteger ret) { + super(prj); + _inputs.set(0,null); + _inputs.add(new ProjARM(prj)); + _bits = 8 << ret.log_size(); + _signed = ret._min < 0; + } + @Override public String op() { return (_signed ? "sext" : "zext")+_bits; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding(Encoding enc) { + short dst = enc.reg(this), src = enc.reg(in(1)); + // SBFM/UBFM aliases: sign/zero extend the low byte, short or int. + enc.add4(Arm64.imm_shift(_signed ? Arm64.OPI_ASR : Arm64.OPI_LSR,0,_bits-1,src,dst)); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/F8ToIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/F8ToIARM.java new file mode 100644 index 00000000..31dcfc36 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/F8ToIARM.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class F8ToIARM extends MachConcreteNode implements MachNode { + F8ToIARM(Node f8toi) { super(f8toi); } + @Override public String op() { return "cvti"; } + @Override public RegMask regmap(int i) { return arm.DMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + enc.add4(Arm64.floatToInteger(enc.reg(in(1))-arm.D_OFFSET,enc.reg(this))); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p("(int)").p(code.reg(in(1))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/FltARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/FltARM.java new file mode 100644 index 00000000..db2540f7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/FltARM.java @@ -0,0 +1,45 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +//FMOV (scalar, immediate) +//Floating-point move immediate. +public class FltARM extends ConstantNode implements MachNode, RIPRelSize { + final String _ext; + FltARM( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { return "ld8"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return arm.DMASK; } + @Override public boolean isClone() { return true; } + @Override public FltARM copy() { return new FltARM(this,_ext); } + + @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); + enc.largeConstant(this,_con,0,-1/*TODO: ARM-style ELF patching*/); + short dst = (short)(enc.reg(this) - arm.D_OFFSET); + enc.add4(Arm64.load_pc(Arm64.OPF_ARM, 0, dst)); + } + + // Delta is from opcode start. + @Override public byte encSize(int delta) { return 4; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short dst = (short)(enc.reg(this) - arm.D_OFFSET); + enc.patch4(opStart, Arm64.load_pc(Arm64.OPF_ARM, delta, dst)); + } + + // 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(" = #")); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/FunARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/FunARM.java new file mode 100644 index 00000000..17cafcde --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/FunARM.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class FunARM extends FunNode implements MachNode { + FunARM(FunNode fun) { super(fun); } + @Override public void computeFrameAdjust(CodeGen code, int maxReg) { + super.computeFrameAdjust(code,maxReg); + _frameAdjust = (_frameAdjust+15) & -16; // Keep SP aligned, including leaf spills. + } + @Override public void encoding( Encoding enc ) { + int sz = _frameAdjust; + if( sz == 0 ) return; // Skip if no frame adjust + if( sz >= 1L<<12 ) throw Utils.TODO(); + enc.add4(Arm64.imm_inst(Arm64.OPI_SUB, sz, arm.RSP, arm.RSP)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/I2F8ARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/I2F8ARM.java new file mode 100644 index 00000000..b1c30afc --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/I2F8ARM.java @@ -0,0 +1,26 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class I2F8ARM extends MachConcreteNode implements MachNode { + I2F8ARM(Node i2f8) { super(i2f8); } + @Override public String op() { return "cvt"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.DMASK; } + @Override public void encoding( Encoding enc ) { + // SCVTF + short self = (short)(enc.reg(this )-arm.D_OFFSET); + short reg1 = enc.reg(in(1)); + int body = Arm64.float_cast(Arm64.OP_FLOAT_C, 1, reg1, self); + enc.add4(body); + } + + // 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))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/IntARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/IntARM.java new file mode 100644 index 00000000..72b76edd --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/IntARM.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +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 com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Integer constants +public class IntARM extends ConstantNode implements MachNode { + final String _ext; + IntARM( ConstantNode con, String ext ) { super(con); _ext = ext; } + + @Override public String op() { return "ldi"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public boolean isClone() { return true; } + @Override public IntARM copy() { return new IntARM(this,_ext); } + + @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); + short self = enc.reg(this); + long x = _con==Type.NIL ? 0 : ((TypeInteger)_con).value(); + Arm64.constant(enc,self,x); + } + + // 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); + _con.print(sb.p(reg).p(" = #")); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LoadARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LoadARM.java new file mode 100644 index 00000000..53494092 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LoadARM.java @@ -0,0 +1,34 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.LoadNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +// Load memory addressing on ARM +// Support imm, reg(direct), or reg+off(indirect) addressing +// Base = base - base pointer, offset is added to base +// idx = null +// off = off - offset added to base + +public class LoadARM extends MemOpARM { + LoadARM(LoadNode ld, Node base, Node idx, int off) { + super(ld, base, idx, off, 0); + } + @Override public String op() { return "ld"+_sz; } + @Override public RegMask outregmap() { return size() >= 4 ? arm.MEM_MASK : arm.WMASK; } + + private static final int[] OP_LOADS = new int[]{ Arm64.OP_LOAD_IMM_8, Arm64.OP_LOAD_IMM_16, Arm64.OP_LOAD_IMM_32, Arm64.OP_LOAD_IMM_64, }; + + private static final int[] OP_LOAD_RS = new int[]{ Arm64.OP_LOAD_R_8 , Arm64.OP_LOAD_R_16 , Arm64.OP_LOAD_R_32 , Arm64.OP_LOAD_R_64, }; + + // ldr(immediate - unsigned offset) | ldr(register) + @Override public void encoding( Encoding enc ) { + ldst_encode(enc, OP_LOADS[Integer.numberOfTrailingZeros(size())], OP_LOAD_RS[_con.log_size()], this, size()); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(","); + asm_address(code,sb); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslARM.java new file mode 100644 index 00000000..954cfac2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Logical Shift Left (register) +public class LslARM extends MachConcreteNode implements MachNode { + LslARM(Node asr) {super(asr);} + @Override public String op() { return "shr"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { arm.shift_reg(enc,this,Arm64.OP_LSL); } + @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))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslIARM.java new file mode 100644 index 00000000..b32153ab --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LslIARM.java @@ -0,0 +1,34 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +public class LslIARM extends MachConcreteNode implements MachNode { + final int _imm; + LslIARM(Node lsl, int imm) { + super(lsl); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return "lsli"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + short rd = enc.reg(this); + short rn = enc.reg(in(1)); + assert _imm > 0; + // UBFM <Xd>, <Xn>, #(-<shift> MOD 64), #(63-<shift>) + // immr must be (-<shift> MOD 64) = 64 - shift + enc.add4(Arm64.imm_shift(Arm64.OPI_LSL, 64 - _imm, (64 - _imm) - 1, rn, rd)); + } + // General form: "lsli rd = rs1 << imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" << #").p(_imm); + } +}
\ No newline at end of file diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrARM.java new file mode 100644 index 00000000..91be34e3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Logical Shift Right (register) +public class LsrARM extends MachConcreteNode implements MachNode { + LsrARM(Node asr) {super(asr);} + @Override public String op() { return "shr"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { arm.shift_reg(enc,this,Arm64.OP_LSR); } + @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))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrIARM.java new file mode 100644 index 00000000..243ce968 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/LsrIARM.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// logical right shift immediate +public class LsrIARM extends MachConcreteNode implements MachNode { + final int _imm; + LsrIARM(Node lsr, int imm) { + super(lsr); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return "lsri"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + short rd = enc.reg(this); + short rn = enc.reg(in(1)); + assert _imm > 0; + enc.add4(Arm64.imm_shift(Arm64.OPI_LSR,_imm, 0b111111, rn,rd)); + } + // General form: "lsri rd = rs1 >>> imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" >>> #").p(_imm); + } +}
\ No newline at end of file diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MemOpARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MemOpARM.java new file mode 100644 index 00000000..14eaf510 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MemOpARM.java @@ -0,0 +1,81 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import java.lang.StringBuilder; + +public abstract class MemOpARM extends MemOpNode implements MachNode { + final int _off; // Limit 9 bits sized, or (13 bits<<logsize) unsigned + final int _imm; // Limit ? bits + final int _bytes; + final char _sz; + MemOpARM(MemOpNode mop, Node ptr, Node idx, int off, int imm) { + super(mop,mop); + assert ptr._type instanceof TypeMemPtr && !(ptr instanceof AddNode); + assert ptr() == ptr; + _inputs.setX(2, ptr); + _inputs.setX(3, idx); + _off = off; + _imm = imm; + _bytes = mop instanceof StoreNode st ? st.storeSize() : 1<<_con.log_size(); + _sz = (char)('0'+_bytes); + } + + // Store-based flavors have a value edge + MemOpARM( MemOpNode mop, Node ptr, Node idx, int off, int imm, Node val ) { + this(mop,ptr,idx,off,imm); + _inputs.setX(4,val); + } + + @Override public String label() { return op();} + Node val() { return in(4); } // Only for stores + + @Override protected ExprPrinter<Node> _printMach(ExprPrinter<Node> p) { return p.p(".").p(_name); } + + @Override public Type compute() { throw new AssertionError("Do not call compute on "+getClass().getSimpleName()); } + @Override public Node idealize() { throw new AssertionError("Do not call idealize on "+getClass().getSimpleName()); } + + int size() { return _bytes; } + + // Wider mask to store both GPRs and FPRs + @Override public RegMask regmap(int i) { + // 0 - ctrl + if( i==1 ) return null; // memory + if( i==2 ) return arm.RMASK; // ptr/base + if( i==3 ) return arm.RMASK; // off/index + if( i==4 ) return size() >= 4 ? arm.MEM_MASK : arm.RMASK; // value + return null; // Anti-dependence + } + + // Shared encoding for loads and stores(int and float) + public void ldst_encode( Encoding enc, int opcode_imm, int opcode_reg, Node xval, int size) { + short ptr = enc.reg(ptr()); + short off = enc.reg(off()); + short val = enc.reg(xval); + // The allocated register selects integer vs SIMD/FP encoding, even for + // bitwise copies. ARM numbers each register bank from zero in the opcode. + boolean fp = val >= arm.D0; + assert !fp || size >= 4; + if( fp ) { + opcode_imm |= 1<<4; + opcode_reg |= 1<<5; + val -= arm.D0; + } + int body = off() == null + ? Arm64.load_str_imm(opcode_imm, _off, ptr, val, size) + : Arm64.indr_adr(opcode_reg, off, Arm64.STORE_LOAD_OPTION.SXTX, 0, ptr, val); + enc.add4(body); + } + + SB asm_address(CodeGen code, SB sb) { + sb.p("[").p(code.reg(ptr())).p("+"); + if( off() != null ) sb.p(code.reg(off())); + else sb.p(_off); + return sb.p("]"); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulARM.java new file mode 100644 index 00000000..f598cefb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// mulh signed multiply instruction(no-imm form) +public class MulARM extends MachConcreteNode implements MachNode { + MulARM(Node mul) {super(mul);} + @Override public String op() { return "mul"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { arm.madd(enc,this,Arm64.OP_MUL,31); } + // General form: "rd = rs1 * rs2" + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulFARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulFARM.java new file mode 100644 index 00000000..a4f81fd4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/MulFARM.java @@ -0,0 +1,19 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class MulFARM extends MachConcreteNode implements MachNode{ + MulFARM(Node mulf) {super(mulf);} + @Override public String op() { return "mulf"; } + @Override public RegMask regmap(int i) { return arm.DMASK; } + @Override public RegMask outregmap() { return arm.DMASK; } + @Override public void encoding( Encoding enc ) { arm.f_scalar(enc,this,Arm64.OPF_MUL); } + // Default on double precision for now(64 bits) + // General form: "VMUL.f32 rd = src1 * src2 + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NegARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NegARM.java new file mode 100644 index 00000000..881b41f9 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NegARM.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +public class NegARM extends MachConcreteNode implements MachNode { + NegARM(Node sub) { + super(sub); + //_inputs.pop(); + } + @Override public String op() { return "neg"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + // reverse subtract with immediate 0 + arm.imm_inst(enc,this, in(1), 0b00100110, 0); + } + // General form: "subi rd = rs1 - imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = -").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NewARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NewARM.java new file mode 100644 index 00000000..8bb8983e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NewARM.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class NewARM extends NewNode implements MachNode, RIPRelSize { + // A pre-zeroed chunk of memory. + NewARM(NewNode nnn) { super(nnn); } + @Override public void encoding( Encoding enc ) { + // BL(branch with link) + enc.external(this,"calloc"). + add4(Arm64.mov(Arm64.OP_MOVZ, 0, 1, 0)). // movz x0,#1 + add4(Arm64.b(Arm64.OP_CALL, 0)); + } + + // Patch is for running "new" in a JIT. + // Delta is from opcode start + @Override public byte encSize(int delta) { return 4*2; } + + // Patch is for running "new" in a JIT. + // Delta is from opcode start + @Override public void patch(Encoding enc, int opStart, int opLen, int delta ) { + // Negative patches are JIT emulator targets. + if( opStart+delta < 0 ) { + enc.patch4(opStart+4, Arm64.b_calloc(Arm64.OP_CALL, delta-4)); + } else { + throw Utils.TODO(); + } + } + // General form: "alloc #bytes PC" + @Override public void asm(CodeGen code, SB sb) { + sb.p("\n"); // Multi-op trigger + sb.p("ldi ").p(code._mach.regs()[_arg2Reg]).p(" = #1\n"); + sb.p("call #calloc"); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NotARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NotARM.java new file mode 100644 index 00000000..f8e450f6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/NotARM.java @@ -0,0 +1,28 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.util.SB; + +public class NotARM extends MachConcreteNode implements MachNode{ + NotARM(NotNode not) {super(not);} + @Override public String op() { return "not"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public RegMask killmap() { return arm.FLAGS_MASK; } + @Override public void encoding( Encoding enc ) { + // subs xzr, rs, #0 + // cset rd, eq // Set rd to 1 if rs == 0 (equal), else 0 + // subtracting zero from rs will just yield rs, it sets the zero flag and then it's used in cset + short self = enc.reg(this ); + short reg1 = enc.reg(in(1)); + int subs = Arm64.imm_inst(Arm64.OP_SUBS, 0, reg1, 31); + enc.add4(subs); + int cset = Arm64.cset(Arm64.OP_CSET, 31, Arm64.COND.EQ, 31, self); + enc.add4(cset); + } + + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(this)); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrARM.java new file mode 100644 index 00000000..5b7264ec --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrARM.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Not using the ORRS variant. +public class OrARM extends MachConcreteNode implements MachNode { + OrARM(Node and) { super(and); } + @Override public String op() { return "or"; } + @Override public String glabel() { return "|"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { arm.r_reg(enc,this,Arm64.OP_OR); } + // General form: #rd = rs1 & rs2 + @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))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrIARM.java new file mode 100644 index 00000000..86d07bee --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/OrIARM.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class OrIARM extends MachConcreteNode implements MachNode { + final int _imm; + OrIARM(Node or, int imm) { + super(or); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return "ori"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { + arm.imm_inst_n(enc, this, in(1), Arm64.OPI_OR, _imm); + } + // General form: "ori rd = rs1 | imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" | #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ParmARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ParmARM.java new file mode 100644 index 00000000..b419fe0a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ParmARM.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ParmNode; + +public class ParmARM extends ParmNode implements MachNode { + final RegMask _rmask; + ParmARM(ParmNode parm) { + super(parm); + _rmask = arm.callInMask(fun().sig(),_idx,0); + } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return _rmask; } + @Override public void encoding( Encoding enc ) { } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ProjARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ProjARM.java new file mode 100644 index 00000000..4b7d6f13 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/ProjARM.java @@ -0,0 +1,15 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ProjNode; + +public class ProjARM extends ProjNode implements MachNode { + ProjARM( ProjNode p ) { super(p); } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { + return ((MachNode)in(0)).outregmap(_idx); + } + @Override public void encoding( Encoding enc ) { } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/RetARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/RetARM.java new file mode 100644 index 00000000..a7ed5073 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/RetARM.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; + +public class RetARM extends ReturnNode implements MachNode { + RetARM(ReturnNode ret, FunNode fun) { super(ret, fun); fun.setRet(this); } + @Override public void encoding( Encoding enc ) { + int frameAdjust = ((FunARM)fun())._frameAdjust; + if( frameAdjust > 0 ) + enc.add4(Arm64.imm_inst(Arm64.OPI_ADD, frameAdjust, arm.RSP, arm.RSP)); + enc.add4(Arm64.ret(Arm64.OP_RET)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SetARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SetARM.java new file mode 100644 index 00000000..e5b06c39 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SetARM.java @@ -0,0 +1,36 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// conditional set? +// e.g CSET, stores flag info into GPRS after compare +public class SetARM extends MachConcreteNode implements MachNode { + final String _bop; // One of <,<=,== + final boolean _fp; // FP compare (unsigned encoding) + SetARM(Node cmp, String bop, boolean fp) { + super(cmp); + _inputs.setLen(1); // Pop the cmp inputs + // Replace with the matched cmp + _inputs.push(cmp); + _bop = bop; + _fp = fp; + } + @Override public String op() { return "set"+_bop; } + @Override public RegMask regmap(int i) { assert i==1; return arm.FLAGS_MASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { + // Using RM==RN==31 implies a *zero* value, not R31. + int body = Arm64.cset(Arm64.OP_CSET,0b11111, Arm64.make_condition(_bop, _fp), 0b011111, enc.reg(this)); + enc.add4(body); + } + + @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); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitARM.java new file mode 100644 index 00000000..dcfeb5d8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitARM.java @@ -0,0 +1,60 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.SplitNode; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class SplitARM extends SplitNode { + SplitARM(LRG lrg, String kind, byte round) { super(lrg,kind,round,new Node[2]);} + @Override public RegMask regmap(int i) { return arm.SPLIT_MASK; } + @Override public RegMask outregmap() { return arm.SPLIT_MASK; } + + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src = enc.reg(in(1)); + + boolean dstX = dst >= arm.D_OFFSET; + boolean srcX = src >= arm.D_OFFSET; + + if (dst >= arm.MAX_REG) { + // store to sp + if(src >= arm.MAX_REG) { + throw Utils.TODO(); + } + int off = fun(enc).computeStackOffset(enc._code,dst); + if( srcX ) src -= arm.D_OFFSET; + enc.add4(Arm64.load_str_imm(Arm64.OP_STORE_IMM_64, off, arm.RSP, src, 8)); + return; + } + + if(src >= arm.MAX_REG) { + // Load from SP + int off = fun(enc).computeStackOffset(enc._code,src); + if( dstX ) dst -= arm.D_OFFSET; + enc.add4(Arm64.load_str_imm(Arm64.OP_LOAD_IMM_64, off, arm.RSP, dst, 8)); + return; + } + + // pick opcode based on regs + if(!dstX && !srcX) { + // GPR->GPR + enc.add4(Arm64.mov_reg(Arm64.OP_MOV, src, dst)); + } else if(dstX && srcX) { + // FPR->FPR + // fmov reg + enc.add4(Arm64.f_mov_reg(Arm64.OP_FMOV_REG, src - arm.D_OFFSET,dst - arm.D_OFFSET)); + } else if(!srcX && dstX) { + // GPR->FPR + // FMOV(general) 64 bits to DOUBLE-PRECISION + enc.add4(Arm64.f_mov_general(Arm64.OP_FMOV, 0b01, 0, 0b111, src, dst - arm.D_OFFSET)); + } else if( srcX && !dstX) { + //FPF->GPR + // FMOV(general) DOUBLE-PRECISION to 64 bits + enc.add4(Arm64.f_mov_general(Arm64.OP_FMOV, 0b01, 0, 0b110, src - arm.D_OFFSET, dst)); + } + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitFlagsARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitFlagsARM.java new file mode 100644 index 00000000..25e5d501 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitFlagsARM.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; + +// NZCV transfers require a GPR. Further splits can spill that GPR to the stack. +public class SplitFlagsARM extends SplitARM { + private static final RegMask MASK = new RegMask(arm.WR_BITS,1L); + SplitFlagsARM(LRG lrg,String kind,byte round) { super(lrg,kind,round); } + @Override public boolean isNonStackSplit() { return true; } + @Override public RegMask regmap(int i) { return MASK; } + @Override public RegMask outregmap() { return MASK; } + + @Override public void encoding(Encoding enc) { + short dst=enc.reg(this), src=enc.reg(in(1)); + if( dst==src ) return; + if( src==arm.FLAGS ) enc.add4(Arm64.flags(dst,false)); + else if( dst==arm.FLAGS ) enc.add4(Arm64.flags(src,true)); + else super.encoding(enc); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitGPRARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitGPRARM.java new file mode 100644 index 00000000..c9b2127a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitGPRARM.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.simple.codegen.*; + +// Keep a saved NZCV value in GPRs and stack slots through subsequent spills. +public class SplitGPRARM extends SplitARM { + private static final RegMask MASK = new RegMask(arm.WR_BITS,-2L); + SplitGPRARM(LRG lrg,String kind,byte round) { super(lrg,kind,round); } + @Override public RegMask regmap(int i) { return MASK; } + @Override public RegMask outregmap() { return MASK; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/StoreARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/StoreARM.java new file mode 100644 index 00000000..0b90ac8b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/StoreARM.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StoreNode; +import com.seaofnodes.simple.util.SB; + +// Store memory addressing on ARM +// Support imm, reg(direct), or reg+off(indirect) addressing +// base - base pointer, offset is added to base +// null - index never allowed (no [reg+reg] mode) +// off - offset added to base +// imm - immediate value to store, only if val is null +// val - value to store or null + +//e.g s.cs[0] = 67; // C +// base = s.cs, off = 4, imm = 67, val = null +public class StoreARM extends MemOpARM { + StoreARM(StoreNode st, Node base, Node idx, int off, Node val) { + super(st, base, idx, off, 0, val); + } + @Override public String op() { return "st"+_sz; } + @Override public RegMask outregmap() { return null; } + + private static final int[] OP_STORES = new int[]{ Arm64.OP_STORE_IMM_8, Arm64.OP_STORE_IMM_16, Arm64.OP_STORE_IMM_32, Arm64.OP_STORE_IMM_64, }; + + private static final int[] OP_STORE_RS = new int[]{ Arm64.OP_STORE_R_8, Arm64.OP_STORE_R_16, Arm64.OP_STORE_R_32, Arm64.OP_STORE_R_64, }; + + @Override public void encoding( Encoding enc ) { + ldst_encode(enc, OP_STORES[Integer.numberOfTrailingZeros(size())], OP_STORE_RS[Integer.numberOfTrailingZeros(_bytes)], val(), size()); + } + @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/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubARM.java new file mode 100644 index 00000000..fd5cd4de --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class SubARM extends MachConcreteNode implements MachNode { + SubARM(Node sub) { super(sub); } + @Override public String op() { return "sub"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { arm.r_reg(enc,this,Arm64.OP_SUB); } + + // General form: "sub # rd = rs1 - rs2" + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubFARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubFARM.java new file mode 100644 index 00000000..1a751434 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubFARM.java @@ -0,0 +1,19 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class SubFARM extends MachConcreteNode implements MachNode { + SubFARM(Node subf) {super(subf);} + @Override public String op() { return "subf"; } + @Override public RegMask regmap(int i) { return arm.DMASK; } + @Override public RegMask outregmap() { return arm.DMASK; } + @Override public void encoding( Encoding enc ) { arm.f_scalar(enc,this,Arm64.OPF_SUB); } + // Default on double precision for now(64 bits) + // General form: "vsub.f32 rd = src1 + src2 + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubIARM.java new file mode 100644 index 00000000..b2c0b2ed --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/SubIARM.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +public class SubIARM extends MachConcreteNode implements MachNode { + final int _imm; + SubIARM(Node sub, int imm) { + super(sub); + _inputs.pop(); + _imm = imm; + } + @Override public String op() { return _imm == -1 ? "dec" : "subi"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + @Override public void encoding( Encoding enc ) { + arm.imm_inst(enc,this, in(1), Arm64.OPI_SUB,_imm); + } + + // General form: "subi rd = rs1 - imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" - #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TFPARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TFPARM.java new file mode 100644 index 00000000..a053ba46 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TFPARM.java @@ -0,0 +1,51 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class TFPARM extends FunPtrNode implements MachNode, RIPRelSize { + final String _ext; + // Pointer to a Simple function + TFPARM( FunPtrNode fptr ) { super(fptr); _ext = null; } + // Pointer to an Extern "C" function + TFPARM( TypeFunPtr fptr, String ext ) { super(fptr,CodeGen.CODE._start,null); _type = fptr; _ext = ext; } + @Override public String op() { return "ldx"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public boolean isClone() { return true; } + @Override public TFPARM copy() { return new TFPARM((TypeFunPtr)_con,_ext); } + @Override public void encoding( Encoding enc ) { + if( _ext==null ) enc.relo(this); // Internal patch + else enc.external(this,_ext); // ELF-file external patch + short dst = enc.reg(this); + // adrp x0, 0 + enc.add4(Arm64.adrp(1,0, Arm64.OP_ADRP, 0,dst)); + // add x0, x0, 0 + arm.imm_inst(enc,Arm64.OPI_ADD,0, dst); + } + + @Override public byte encSize(int delta) { + return 8; + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short rpc = enc.reg(this); + if(opLen == 8 ) { + arm.patch_adrp_add(enc, opStart, delta, rpc); + } else { + // should not happen as one instruction is 4 byte, and TFP arm encodes 2. + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + _type.print(sb.p(reg).p(" #")); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java new file mode 100644 index 00000000..b2531f97 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeMemPtr; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class TMPARM extends ConstantNode implements MachNode, RIPRelSize { + final String _ext; + TMPARM( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { return "ldp"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public boolean isClone() { return true; } + @Override public TMPARM copy() { return new TMPARM(this,_ext); } + @Override public void encoding( Encoding enc ) { + if( _ext!=null ) enc.externalData(this,_ext); + else enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1); + short dst = enc.reg(this); + // adrp x0, 0 + enc.add4(Arm64.adrp(1,0, Arm64.OP_ADRP, 0,dst)); + // add x0, x0, 0 + arm.imm_inst(enc,Arm64.OPI_ADD,0, dst); + } + + @Override public byte encSize(int delta) { + return 8; + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short dst = enc.reg(this); + if(opLen == 8 ) { + arm.patch_adrp_add(enc, opStart, delta, dst); + } else { + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + _con.print(sb.p(reg).p(" #")); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/UJmpARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/UJmpARM.java new file mode 100644 index 00000000..aecd02bb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/UJmpARM.java @@ -0,0 +1,48 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// unconditional jump +public class UJmpARM extends CFGNode implements MachNode, RIPRelSize { + UJmpARM() { } + @Override public String op() { return "jmp"; } + @Override public String label() { return op(); } + @Override protected String format() { return "jmp "; } + @Override public RegMask regmap(int i) {return null; } + @Override public RegMask outregmap() { return null; } + @Override public Type compute() { throw Utils.TODO(); } + @Override public Node idealize() { throw Utils.TODO(); } + @Override public void encoding( Encoding enc ) { + enc.jump(this,uctrl()); + int body = Arm64.b(Arm64.OP_UJMP, 0); + enc.add4(body); + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { + if( -(1<<26) <= delta && delta < (1<<26) ) return 4; + // 2 word encoding needs a tmp register, must teach RA + throw Utils.TODO(); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + if( opLen==4 ) { + enc.patch4(opStart,Arm64.b(Arm64.OP_UJMP, delta)); + } else { + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + CFGNode target = uctrl(); + //assert target.nOuts() > 1; // Should optimize jmp to empty targets + sb.p(label(target)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorARM.java new file mode 100644 index 00000000..d56397a3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorARM.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class XorARM extends MachConcreteNode implements MachNode{ + XorARM(Node xor) {super(xor);} + @Override public String op() { return "xor"; } + @Override public String glabel() { return "^"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + @Override public void encoding( Encoding enc ) { arm.r_reg(enc,this,Arm64.OP_XOR); } + // General form: "rd = x1 ^ x2" + @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))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorIARM.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorIARM.java new file mode 100644 index 00000000..12143ccd --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/XorIARM.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; + +public class XorIARM extends MachConcreteNode implements MachNode { + final int _imm; + XorIARM(Node xor, int imm) { + super(xor); + _inputs.pop(); + _imm = imm; + } + + @Override public String op() { return "xori"; } + @Override public RegMask regmap(int i) { return arm.RMASK; } + @Override public RegMask outregmap() { return arm.WMASK; } + + // General form: "xori rd = rs1 ^ imm" + @Override public void encoding( Encoding enc ) { arm.imm_inst_n(enc,this, in(1), Arm64.OPI_XOR,_imm); } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" ^ #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java new file mode 100644 index 00000000..3c965da5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java @@ -0,0 +1,503 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import static com.seaofnodes.isa.Arm64.*; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +public class arm extends Machine { + public arm( CodeGen code ) { + if( !"SystemV".equals(code._callingConv) ) + throw new IllegalArgumentException("Unknown calling convention "+code._callingConv); + } + + // ARM64 + @Override public String name() { return "arm"; } + + // GPR(S) + public static final int X0 = 0, X1 = 1, X2 = 2, X3 = 3, X4 = 4, X5 = 5, X6 = 6, X7 = 7; + public static final int X8 = 8, X9 = 9, X10 = 10, X11 = 11, X12 = 12, X13 = 13, X14 = 14, X15 = 15; + public static final int X16 = 16, X17 = 17, X18 = 18, X19 = 19, X20 = 20, X21 = 21, X22 = 22, X23 = 23; + public static final int X24 = 24, X25 = 25, X26 = 26, X27 = 27, X28 = 28, X29 = 29, X30 = 30, RSP = 31; + + // Floating point registers + public static final int D0 = 32, D1 = 33, D2 = 34, D3 = 35, D4 = 36, D5 = 37, D6 = 38, D7 = 39; + public static final int D8 = 40, D9 = 41, D10 = 42, D11 = 43, D12 = 44, D13 = 45, D14 = 46, D15 = 47; + public static final int D16 = 48, D17 = 49, D18 = 50, D19 = 51, D20 = 52, D21 = 53, D22 = 54, D23 = 55; + public static final int D24 = 56, D25 = 57, D26 = 58, D27 = 59, D28 = 60, D29 = 61, D30 = 62, D31 = 63; + + static final int FLAGS = 64; + static final int MAX_REG = 65; + public static final int D_OFFSET = 32; + + static final String[] REGS = new String[] { + "X0", "X1", "X2", "X3", "X4", "X5", "X6", "X7", + "X8", "X9", "X10", "X11", "X12", "X13", "X14", "X15", + "X16", "X17", "X18", "X19", "X20", "X21", "X22", "X23", + "X24", "X25", "X26", "X27", "X28", "X29", "RPC", "RSP", + "D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", + "D8", "D9", "D10", "D11", "D12", "D13", "D14", "D15", + "D16", "D17", "D18", "D19", "D20", "D21", "D22", "D23", + "D24", "D25", "D26", "D27", "D28", "D29", "D30", "D31", + "flags" + }; + @Override public String[] regs() { return REGS; } + + // from (x0-x30) + // General purpose register mask: pointers and ints, not floats + static final long RD_BITS = 0xFFFFFFFFL; + static final RegMask RMASK = new RegMask(RD_BITS); + static final long WR_BITS = 0x7FFFFFFFL; // All the GPRs, not RSP + static final RegMask WMASK = new RegMask(WR_BITS); + + // Float mask from(d0–d31) + static final long FP_BITS = 0xFFFFFFFFL<<D0; + static final RegMask DMASK = new RegMask(FP_BITS); + + // Load/store mask; both GPR and FPR + static final RegMask MEM_MASK = new RegMask(WR_BITS | FP_BITS); + + static final RegMask SPLIT_MASK = new RegMask(WR_BITS | FP_BITS, -2L/*skip flags*/); + + static final RegMask FLAGS_MASK = new RegMask(FLAGS); + + // Arguments masks + static final RegMask X0_MASK = new RegMask(X0); + static final RegMask X1_MASK = new RegMask(X1); + static final RegMask X2_MASK = new RegMask(X2); + static final RegMask X3_MASK = new RegMask(X3); + static final RegMask X4_MASK = new RegMask(X4); + static final RegMask X5_MASK = new RegMask(X5); + static final RegMask X6_MASK = new RegMask(X6); + static final RegMask X7_MASK = new RegMask(X7); + + // Arguments(float) masks + static final RegMask D0_MASK = new RegMask(D0); + static final RegMask D1_MASK = new RegMask(D1); + static final RegMask D2_MASK = new RegMask(D2); + static final RegMask D3_MASK = new RegMask(D3); + static final RegMask D4_MASK = new RegMask(D4); + static final RegMask D5_MASK = new RegMask(D5); + static final RegMask D6_MASK = new RegMask(D6); + static final RegMask D7_MASK = new RegMask(D7); + + // 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 final RegMask[] CALLINMASK = new RegMask[] { + X0_MASK, + X1_MASK, + X2_MASK, + X3_MASK, + X4_MASK, + X5_MASK, + X6_MASK, + X7_MASK, + }; + static final RegMask[] XMMS = new RegMask[] { + D0_MASK, + D1_MASK, + D2_MASK, + D3_MASK, + D4_MASK, + D5_MASK, + D6_MASK, + D7_MASK, + }; + + // ARM ENCODING + + // True if signed 9-bit immediate + private static boolean imm9(TypeInteger ti) { + // 55 = 64-9 + return ti.isConstant() && ((ti.value()<<55)>>55) == ti.value(); + } + // True if signed 12-bit immediate + private static boolean imm12(TypeInteger ti) { + // 52 = 64-12 + return ti.isConstant() && ((ti.value()<<52)>>52) == ti.value(); + } + + // Can we encode this in ARM's 12-bit LOGICAL immediate form? + // Some combination of shifted bit-masks. + private static int imm12Logical(TypeInteger ti) { + return ti.isConstant() ? Arm64.logicalImmediate(ti.value()) : -1; + } + + public static void imm_inst(Encoding enc, Node n,Node n2, int opcode, int imm12) { + short self = enc.reg(n); + short reg1 = enc.reg(n2); + + // ADD/SUB encode an unsigned magnitude; switch operation for a negative addend. + if( imm12<0 && (opcode==OPI_ADD || opcode==OPI_SUB) ) { + opcode ^= OPI_ADD ^ OPI_SUB; + imm12 = -imm12; + } + int body = Arm64.imm_inst(opcode, imm12&0xFFF, reg1, self); + enc.add4(body); + } + + public static void imm_inst_subs(Encoding enc, Node n,Node n2, int opcode, int imm12) { + short reg1 = enc.reg(n2); + // 31 = 11111 + int body = Arm64.imm_inst(opcode, imm12&0xFFF, reg1, 31); + enc.add4(body); + } + + // for cases where rs1 and dst are the same, eg add x0, x0, 1 + public static void imm_inst(Encoding enc, int opcode, int imm12, int self) { + int body = Arm64.imm_inst(opcode, imm12&0xFFF, self, self); + enc.add4(body); + } + + public static void imm_inst_n(Encoding enc, Node n, Node n2, int opcode, int imm13) { + short self = enc.reg(n); + short reg1 = enc.reg(n2); + int body = Arm64.imm_inst_n(opcode, imm13, reg1, self); + enc.add4(body); + } + + // for cases where rs1 and dst are the same, eg add x0, x0, 1 + public static int imm_inst_l(Encoding enc, Node n, int opcode, int imm12) { + short self = enc.reg(n); + short reg1 = enc.reg(n.in(1)); + int body = Arm64.imm_inst(opcode, imm12&0xFFF, reg1, self); + return body; + } + + // for normal add, reg1, reg2 cases (reg-to-reg) + // using shifted-reg form + + public static void r_reg(Encoding enc, Node n, int opcode) { + short self = enc.reg(n); + short reg1 = enc.reg(n.in(1)); + short reg2 = enc.reg(n.in(2)); + int body = Arm64.r_reg(opcode, 0, reg2, 0, reg1, self >= 32 ? reg1: self); + enc.add4(body); + } + + public static void r_reg_subs(Encoding enc, Node n, int opcode) { + short reg1 = enc.reg(n.in(1)); + short reg2 = enc.reg(n.in(2)); + // 31 = 11111 + int body = Arm64.r_reg(opcode, 0, reg2, 0, reg1, 31); + enc.add4(body); + } + + public static void shift_reg(Encoding enc, Node n, int op2) { + short self = enc.reg(n); + short reg1 = enc.reg(n.in(1)); + short reg2 = enc.reg(n.in(2)); + int body = Arm64.shift_reg(0b10011010110, reg2, op2, reg1, self); + enc.add4(body); + } + + // MUL can be considered an alias for MADD with the third operand Ra being set to 0 + + public static void madd(Encoding enc, Node n, int opcode, int ra) { + short self = enc.reg(n); + short reg1 = enc.reg(n.in(1)); + short reg2 = enc.reg(n.in(2)); + int body = Arm64.madd(opcode, reg2, ra, reg1, self); + enc.add4(body); + } + + public static void f_scalar(Encoding enc, Node n, int op ) { + short self = (short)(enc.reg(n) -D_OFFSET); + short reg1 = (short)(enc.reg(n.in(1))-D_OFFSET); + short reg2 = (short)(enc.reg(n.in(2))-D_OFFSET); + int body = Arm64.f_scalar(0b00011110, 1, reg2, op, reg1, self); + enc.add4(body); + } + + public static void patch_adrp_add(Encoding enc, int opStart, int delta, int rd) { + int target = opStart + delta; + int pageDelta = ((target & ~0xFFF) - (opStart & ~0xFFF)) >> 12; + int imm21 = pageDelta & 0x1FFFFF; + enc.patch4(opStart , Arm64.adrp(1, imm21 & 3, OP_ADRP, imm21 >>> 2, rd)); + enc.patch4(opStart+4, Arm64.imm_inst_l(OPI_ADD, target & 0xFFF, rd)); + } + + public static void f_cmp(Encoding enc, Node n) { + short reg1 = (short)(enc.reg(n.in(1))-D_OFFSET); + short reg2 = (short)(enc.reg(n.in(2))-D_OFFSET); + int body = Arm64.f_cmp(0b00011110, 3, reg2, reg1); + enc.add4(body); + } + + @Override public RegMask callArgMask(TypeFunPtr tfp, int idx, int maxArgSlot ) { return callInMask(tfp,idx,maxArgSlot); } + static RegMask callInMask(TypeFunPtr tfp, int idx, int maxArgSlot ) { + if( idx==0 ) return CodeGen.CODE._rpcMask; + if( idx==1 ) return null; + // Count floats in signature up to index + if( idx-2 >= tfp.nargs() ) return null; // Anti-dependence + boolean hidden = hiddenSelf(tfp); + int sigidx = idx-2; + if( hidden ) { + if( sigidx==0 ) return null; + sigidx--; + } + // Count floats in signature up to index + int fcnt=0; + for( int i=hidden ? 1 : 0; i<idx-2; i++ ) + if( tfp.arg(i) instanceof TypeFloat) + fcnt++; + // Floats up to XMMS in XMM registers + if( tfp.arg(idx-2) instanceof TypeFloat ) { + if( fcnt < XMMS.length ) + return XMMS[fcnt]; + } else { + RegMask[] cargs = CALLINMASK; + if( sigidx-fcnt < cargs.length ) + return cargs[sigidx-fcnt]; + } + // Pass on stack slot (8 and higher) + if( maxArgSlot>0 ) throw Utils.TODO(); + return new RegMask(MAX_REG + 1 + sigidx); + } + + // Return the max stack slot used by this signature, or 0 + @Override public short maxArgSlot( TypeFunPtr tfp ) { + int icnt=0, fcnt=0; // Count of ints, floats + for( int i=hiddenSelf(tfp) ? 1 : 0; i<tfp.nargs(); i++ ) { + if( tfp.arg(i) instanceof TypeFloat ) fcnt++; + else icnt++; + } + int nstk = Math.max(icnt-8,0)+Math.max(fcnt-8,0); + return (short)nstk; + } + private static boolean hiddenSelf(TypeFunPtr tfp) { + return tfp.nargs() > 0 && tfp.arg(0) == TypePtr.PTR; + } + + private static final long CALLEE_SAVE = + 1L<<X19 | + 1L<<X20 | 1L<<X21 | 1L<<X22 | 1L<<X23 | + 1L<<X24 | 1L<<X25 | 1L<<X26 | 1L<<X27 | + 1L<<X28 | + 1L<<D9 | 1L<<D10 | 1L<<D11 | + 1L<<D12 | 1L<<D13 | 1L<<D14 | 1L<<D15; + static final long CALLER_SAVE = ~CALLEE_SAVE & ~(1L<<RSP); + @Override public long callerSave() { return CALLER_SAVE; } + @Override public long neverSave() { return 1L<<RSP; } + @Override public RegMask retMask( TypeFunPtr tfp ) { + return tfp.ret() instanceof TypeFloat ? D0_MASK : X0_MASK; + } + @Override public int rpc() { return X30; } + + // Create a split op; any register to any register, including stack slots + @Override public SplitNode split(LRG lrg, String kind, byte round) { + RegMask def = lrg._machDef==null ? null : lrg._machDef.outregmap(), use = lrg.useMask(); + if( (def!=null && def.size1() && def.test(FLAGS)) || + (use!=null && use.size1() && use.test(FLAGS)) ) + return new SplitFlagsARM(lrg,kind,round); + // Spilling the saved flags requires a GPR between NZCV and the stack. + if( lrg._machDef instanceof SplitFlagsARM || lrg._machDef instanceof SplitGPRARM ) + return new SplitGPRARM(lrg,kind,round); + return new SplitARM(lrg,kind,round); + } + + // Return a MachNode unconditional branch + @Override public CFGNode jump() { + return new UJmpARM(); + } + + // Instruction selection + @Override public Node instSelect(Node n ) { + return switch( n ) { + case AddNode add -> add(add); + case AndNode and -> and(and); + case BoolNode bool -> cmp(bool); + case CallNode call -> call(call); + case PtrToIntNode ptr -> new PtrToIntMach(ptr); + case GuardNode guard -> new GuardMach(guard); + case CallEndNode cend-> new CallEndMach(cend); + case CLInitNode init -> new CLInitNode(init); + case CProjNode c -> new CProjNode(c); + case ConstantNode con-> con(con); + case DivNode div -> div.mode()==2 ? new DivFARM(div) : new DivARM(div); + case EscapeNode esc -> new EscapeNode(esc); + case FunNode fun -> new FunARM(fun); + case FunPtrNode fptr -> fptr(fptr); + case IfNode iff -> jmp(iff); + case LoadNode ld -> ld(ld); + case MemMergeNode mem-> new MemMergeNode(mem); + case MinusNode neg -> { + if( neg.mode()==2 ) throw Utils.TODO(); + yield new NegARM(neg); + } + case MulNode mul -> mul.mode()==2 ? new MulFARM(mul) : new MulARM(mul); + case NewNode nnn -> new NewARM(nnn); + case NotNode not -> new NotARM(not); + case OrNode or -> or(or); + case ParmNode parm -> new ParmARM(parm); + case BulkMemPhiNode phi -> new BulkMemPhiNode(phi); + case MemPhiNode phi -> new MemPhiNode(phi); + case PhiNode phi -> new PhiNode(phi); + case ProjNode prj -> prj(prj); + case ReadOnlyNode read -> new ReadOnlyMach(read); + case ReturnNode ret -> new RetARM(ret,ret.fun()); + case SarNode sar -> asr(sar); + case ShlNode shl -> lsl(shl); + case ShrNode shr -> lsr(shr); + case StartCUNode start -> new StartCUNode(start); + case StopCUNode stop -> new StopCUNode(stop); + case StartNode start -> new StartNode(start); + case StopNode stop -> new StopNode(stop); + case StoreNode st -> st(st); + case SubNode sub -> sub.mode()==2 ? new SubFARM(sub) : sub(sub); + case ToFloatNode tfn -> new I2F8ARM(tfn); + case ToIntegerNode cvt -> new F8ToIARM(cvt); + case XorNode xor -> xor(xor); + + case LoopNode loop -> new LoopNode(loop); + case RegionNode region-> new RegionNode(region); + default -> throw Utils.TODO(); + }; + } + + private Node cmp(BoolNode bool){ + Node cmp = _cmp(bool); + return new SetARM(cmp, bool.op(), cmp instanceof CmpFARM); + } + private Node _cmp(BoolNode bool) { + if( bool.isFloat() ) + return new CmpFARM(bool); + return bool.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti + ? new CmpIARM(bool, (int)ti.value()) + : new CmpARM(bool); + } + + private Node ld(LoadNode ld) { + return new LoadARM(address(ld), ld.ptr(), idx, off); + } + + 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 + String op = "!="; + boolean fp = false; + if( iff.in(1) instanceof BoolNode bool ) { op = bool.op(); fp = bool.isFloat(); } + else if( iff.in(1)==null ) op = "=="; // Never-node cutout + else iff.setDef(1, new BoolNode.NE(iff.in(1), + ConstantNode.raw(TypeInteger.ZERO), + (byte)(iff.in(1)._type instanceof TypeFloat ? 2 : 1))); + return new BranchARM(iff, op, fp); + } + + private Node add(AddNode add) { + if( add.mode()==2 ) + return new AddFARM(add); + return add.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && imm12(ti) + ? new AddIARM(add, (int)ti.value()) + : new AddARM(add); + } + + private Node sub(SubNode sub) { + return sub.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti) + ? new SubIARM(sub, (int)(ti.value())) + : new SubARM(sub); + } + + private Node con( ConstantNode con ) { + if( !con._con.isConstant() ) + return ConstantNode.raw(con); // Default unknown caller inputs + String ext = con instanceof ExternNode ext0 ? ext0._extern : null; + return switch( con._con ) { + case TypeInteger ti -> new IntARM(con,ext); + case TypeFloat tf -> new FltARM(con,ext); + case TypeMemPtr tmp -> new TMPARM(con,ext); + case TypeFunPtr tfp -> new TFPARM(tfp,ext); + case TypeNil tn -> throw Utils.TODO(); + // TOP, BOTTOM, XCtrl, Ctrl, etc. Never any executable code. + case Type t -> t==Type.NIL ? new IntARM(con,ext) : ConstantNode.raw(con); + }; + } + + private Node fptr( FunPtrNode con ) { + return new TFPARM(con); + } + + private Node call(CallNode call){ + return call.fptr() instanceof FunPtrNode fptr + ? new CallARM(call, (TypeFunPtr)fptr._type) + : call.fptr() instanceof ConstantNode con + ? new CallARM(call, (TypeFunPtr)con._type) + : new CallRRARM(call); + } + + private Node or(OrNode or) { + int imm12; + return or.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && (imm12 = imm12Logical(ti)) != -1 + ? new OrIARM(or, imm12) + : new OrARM(or); + } + + private Node xor(XorNode xor) { + int imm12; + return xor.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && (imm12 = imm12Logical(ti)) != -1 + ? new XorIARM(xor, imm12) + : new XorARM(xor); + } + + private Node and(AndNode and) { + int imm12; + return and.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && (imm12 = imm12Logical(ti)) != -1 + ? new AndIARM(and, imm12) + : new AndARM(and); + } + + private Node asr(SarNode asr) { + return asr.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && ti.value() >= 0 && ti.value() < 63 + ? new AsrIARM(asr, (int)ti.value()) + : new AsrARM(asr); + } + + private Node lsl(ShlNode lsl) { + return lsl.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && ti.value() >= 0 && ti.value() < 63 + ? new LslIARM(lsl, (int)ti.value()) + : new LslARM(lsl); + } + + private Node lsr(ShrNode lsr) { + return lsr.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && ti.value() >= 0 && ti.value() < 63 + ? new LsrIARM(lsr, (int)ti.value()) + : new LsrARM(lsr); + } + + private Node prj(ProjNode prj) { + if( prj.in(0) instanceof CLInitNode ) return new ProjNode(prj); + TypeInteger ret = cReturn(prj); + if( ret!=null ) return new ExtendARM(prj,ret); + return prj.in(0) instanceof StartNode ? new ProjNode(prj) : new ProjARM(prj); + } + + private static int off; + private static Node idx; + private Node st(StoreNode st) { + return new StoreARM(address(st),st.ptr(),idx,off,st.val()); + } + + // Gather 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 <N extends MemOpNode> N address(N mop ) { + off = 0; // Reset + idx = 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 ConstantNode con && con._con instanceof TypeInteger ti && imm9(ti) ) { + off = (int)ti.value(); + assert off == ti.value(); // In 32-bit range + } else { + idx = mop.off(); + } + return mop; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AUIPC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AUIPC.java new file mode 100644 index 00000000..59bad64e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AUIPC.java @@ -0,0 +1,42 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.FunPtrNode; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Add upper 20bits to PC. Immediate comes from the relocation info. +public class AUIPC extends FunPtrNode implements MachNode, RIPRelSize { + AUIPC( FunPtrNode fptr ) { super(fptr); } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public AUIPC copy() { return new AUIPC(this); } + @Override public String op() { return "auipc"; } + @Override public void encoding( Encoding enc ) { + enc.relo(this); + short dst = enc.reg(this); + enc.add4(RiscV.u_type(RiscV.OP_AUIPC, dst, 0)); + } + + // Delta is from opcode start, but X86 measures from the end of the 5-byte encoding + @Override public byte encSize(int delta) { return 4; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + //short rpc = enc.reg(this); + //// High half is where the TFP constant used to be, the last input + //short auipc = enc.reg(in(_inputs._len-1)); + //enc.patch4(opStart,RiscV.i_type(0x67, rpc, 0, auipc, delta)); + throw Utils.TODO(); + } + + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + sb.p(reg).p(" = PC+#"); + _type.print(sb); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddFRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddFRISC.java new file mode 100644 index 00000000..d87e49b9 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddFRISC.java @@ -0,0 +1,18 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// fadd.d +public class AddFRISC extends MachConcreteNode implements MachNode { + AddFRISC(Node addf) {super(addf);} + @Override public String op() { return "addf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return riscv.FMASK; } + @Override public RegMask outregmap() { return riscv.FMASK; } + @Override public void encoding( Encoding enc ) { riscv.rf_type(enc,this,RiscV.RM.RNE,1); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddIRISC.java new file mode 100644 index 00000000..8012b61e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddIRISC.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class AddIRISC extends ImmRISC { + // Used to inst-selection as a direct match against an ideal Add/Sub + public AddIRISC( Node add, int imm12, boolean pop ) { super(add,imm12,pop); } + @Override public String op() { return "addi"; } + @Override public String glabel() { return "+"; } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() {return 0;} + @Override public AddIRISC copy() { + // Clone the AddI, using the same inputs-only code used during inst select. + // Output edges are missing. + AddIRISC add = new AddIRISC(in(1),_imm12,false); + // Copys happen when output edges should be valid, so correct missing output edges. + in(1)._outputs.add(add); + return add; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddRISC.java new file mode 100644 index 00000000..7010916f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AddRISC.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AddRISC extends MachConcreteNode implements MachNode { + public AddRISC( Node add ) { super(add); } + AddRISC( Node base, Node off ) { + super(new Node[3]); + _inputs.set(1,base); + _inputs.set(2,off ); + } + @Override public String op() { return "add"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,0,0); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndIRISC.java new file mode 100644 index 00000000..9fd65e7d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndIRISC.java @@ -0,0 +1,13 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class AndIRISC extends ImmRISC { + public AndIRISC( Node and, int imm) { super(and,imm); } + @Override public String op() { return "andi"; } + @Override public String glabel() { return "&"; } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() {return 7;} +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndRISC.java new file mode 100644 index 00000000..1845985e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AndRISC.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AndRISC extends MachConcreteNode implements MachNode { + public AndRISC(Node and) { super(and); } + @Override public String op() { return "and"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,0b111,0); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BranchRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BranchRISC.java new file mode 100644 index 00000000..a9d03da5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BranchRISC.java @@ -0,0 +1,95 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Conditional branch such as: BEQ +public class BranchRISC extends IfNode implements MachNode, RIPRelSize { + String _bop; + // label is obtained implicitly + public BranchRISC( IfNode iff, String bop, Node n1, Node n2 ) { + super(iff); + _bop = bop; + _inputs.setX(1,n1); + _inputs.setX(2,n2); + } + + @Override public String op() { return "b"+_bop; } + @Override public String label() { return op(); } + @Override public String comment() { return "L"+cproj(1)._nid; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return null; } + @Override public void negate() { + _bop = negate(_bop); + // Cannot encode ">" or "<=", so flip these + if( _bop.equals(">") || _bop.equals("<=") ) { + swap12(); // Cannot negate the test, so swap the operands + _bop = swap(_bop); // Swap test to match + } + } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + p.p("if( "); + if( in(1)==null ) p.p("0"); + else p.n(in(1)); + p.p(_bop); + if( in(2)==null ) p.p("0"); + else p.n(in(2)); + return p.p(" )"); + } + + @Override public void encoding( Encoding enc ) { + if( in(1)==null && _bop=="!=" ) return; // Inverted never-node, no code + enc.jump(this,cproj(0)); + if( in(1)==null ) // Never node + enc.add4(RiscV.j_type(RiscV.OP_JAL, 0, 0)); + else { + // Todo: relocs (for offset - immf) + short src1 = enc.reg(in(1)); + short src2 = in(2)==null ? (short)riscv.ZERO : enc.reg(in(2)); + enc.add4(RiscV.b_type(RiscV.OP_BRANCH, RiscV.jumpop(_bop), src1, src2, 0)); + } + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { + if( in(1)==null && _bop=="!=" ) return 0; // Inverted never-node, no code + if( -4*1024 <= delta && delta < 4*1024 ) return 4; + // 2 word encoding needs a tmp register, must teach RA + throw Utils.TODO(); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + assert !( in(1)==null && _bop=="!=" ); // Inverted never-node, no code no patch + if( in(1)==null ) { // Never node + enc.patch4(opStart,RiscV.j_type(RiscV.OP_JAL, 0, delta)); + } else { + short src1 = enc.reg(in(1)); + short src2 = in(2)==null ? (short)riscv.ZERO : enc.reg(in(2)); + if( opLen==4 ) { + enc.patch4(opStart,RiscV.b_type(RiscV.OP_BRANCH, RiscV.jumpop(_bop), src1, src2, delta)); + } else { + throw Utils.TODO(); + } + } + } + + @Override public void asm(CodeGen code, SB sb) { + if( in(1)==null && _bop=="!=" ) { + sb.p("never"); + return; + } + String src1 = in(1)==null ? "#0" : code.reg(in(1)); + String src2 = in(2)==null ? "#0" : code.reg(in(2)); + sb.p(src1).p(" ").p(_bop).p(" ").p(src2).p(" "); + CFGNode prj = cproj(0).uctrlSkipEmpty(); + if( !prj.blockHead() ) prj = prj.cfg0(); + sb.p(label(prj)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRISC.java new file mode 100644 index 00000000..b1426a3f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRISC.java @@ -0,0 +1,62 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class CallRISC extends CallNode implements MachNode, RIPRelSize { + final TypeFunPtr _tfp; + final String _name; + CallRISC( CallNode call, TypeFunPtr tfp ) { + super(call); + assert tfp.isConstant(); + _inputs.pop(); // Pop constant target + _tfp = tfp; + _name = CodeGen.CODE.funcName(tfp.fidx()); + } + + @Override public String op() { return "call"; } + @Override public String label() { return op(); } + @Override public String name() { return _name; } + @Override public TypeFunPtr tfp() { return _tfp; } + @Override public RegMask regmap(int i) { return riscv.callInMask(_tfp,i,fun()._maxArgSlot); } + @Override public RegMask outregmap() { return riscv.RPC_MASK; } + @Override public int nargs() { return nIns()-2; } // Minus control, memory, fptr + + @Override public void encoding( Encoding enc ) { + // Short form +/-4K: beq r0,r0,imm12 + // Long form: auipc rX,imm20/32; jal r0,[rX+imm12/32] + FunNode fun = CodeGen.CODE.link(_tfp); + if( fun==null ) enc.external(this,_name); + else enc.relo(this); + short rpc = enc.reg(this); + enc.add4(RiscV.j_type(RiscV.OP_JAL, rpc, 0)); + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { + if( -(1L<<20) <= delta && delta < (1L<<20) ) return 4; + // 2 word encoding needs a tmp register, must teach RA + throw Utils.TODO(); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short rpc = enc.reg(this); + if( opLen==4 ) { + enc.patch4(opStart,RiscV.j_type(RiscV.OP_JAL, rpc, delta)); + } else { + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(_name).p(" "); + for( int i=0; i<nargs(); i++ ) + sb.p(code.reg(arg(i+2))).p(", "); + sb.unchar(2); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRRISC.java new file mode 100644 index 00000000..cda4bc79 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRRISC.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class CallRRISC extends CallNode implements MachNode { + CallRRISC( CallNode call ) { super(call); } + @Override public String op() { return "callr"; } + @Override public String label() { return op(); } + @Override public RegMask regmap(int i) { + return i==_inputs._len-1 + ? riscv.RMASK // Function call target + : riscv.callInMask(tfp(),i,fun()._maxArgSlot); // Normal argument + } + @Override public RegMask outregmap() { return riscv.RPC_MASK; } + + @Override public void encoding( Encoding enc ) { + short rpc = enc.reg(this); + short src = enc.reg(in(_inputs._len-1)); + int body = RiscV.i_type(RiscV.OP_JALR, rpc, 0, src, 0); + enc.add4(body); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(fptr())).p(" "); + for( int i=0; i<nargs(); i++ ) + sb.p(code.reg(arg(i))).p(", "); + sb.unchar(2); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/DivFRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/DivFRISC.java new file mode 100644 index 00000000..8eb4fb3b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/DivFRISC.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// fdiv.d +public class DivFRISC extends MachConcreteNode implements MachNode{ + DivFRISC(Node divf) {super(divf);} + @Override public String op() { return "divf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return riscv.FMASK; } + @Override public RegMask outregmap() { return riscv.FMASK; } + @Override public void encoding( Encoding enc ) { riscv.rf_type(enc,this,RiscV.RM.RUP,13); } + // Default on double precision for now(64 bits) + // General form: "fdiv.d rd = src1 / src2 + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/DivRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/DivRISC.java new file mode 100644 index 00000000..515ffa47 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/DivRISC.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; + +public class DivRISC extends MachConcreteNode implements MachNode{ + public DivRISC(Node div) {super(div);} + @Override public String op() { return "div"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,4,1); } + @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))); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java new file mode 100644 index 00000000..5bd3ec1c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +// Convert a narrow C return register to Simple's full-width integer value. +public class ExtendRISC extends MachConcreteNode { + final int _bits; + final boolean _signed; + ExtendRISC(ProjNode prj, TypeInteger ret) { + super(prj); + _inputs.set(0,null); + _inputs.add(new ProjRISC(prj)); + _bits = 8 << ret.log_size(); + _signed = ret._min < 0; + } + @Override public String op() { return (_signed ? "sext" : "zext")+_bits; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding(Encoding enc) { + short dst = enc.reg(this), src = enc.reg(in(1)); + int shift = 64-_bits; + enc.add4(RiscV.i_type(RiscV.OP_IMM,dst,1,src,shift)); + enc.add4(RiscV.i_type(RiscV.OP_IMM,dst,5,dst,shift | (_signed ? 0x400 : 0))); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/F8ToIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/F8ToIRISC.java new file mode 100644 index 00000000..a56ad216 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/F8ToIRISC.java @@ -0,0 +1,19 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class F8ToIRISC extends MachConcreteNode implements MachNode { + F8ToIRISC(Node f8toi) {super(f8toi);} + @Override public String op() { return "cvti"; } + @Override public RegMask regmap(int i) { assert i==1; return riscv.FMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { + RiscV.floatToInteger(enc,enc.reg(this),enc.reg(in(1))-riscv.F_OFFSET); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p("(int)").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FltRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FltRISC.java new file mode 100644 index 00000000..a110d156 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FltRISC.java @@ -0,0 +1,48 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class FltRISC extends ConstantNode implements MachNode, RIPRelSize { + final String _ext; + FltRISC( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { return "ld8"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.FMASK; } + @Override public boolean isClone() { return true; } + @Override public FltRISC copy() { return new FltRISC(this,_ext); } + + @Override public void encoding( Encoding enc ) { + enc.largeConstant(this,_con, 0, -1); + short dst = (short)(enc.reg(this) - riscv.F_OFFSET); + short tmp = (short)riscv.T6; + // AUIPC dst,#hi20_constant_pool + enc.add4(RiscV.u_type(RiscV.OP_AUIPC, tmp, 0)); + // Load dst,[dst+#low12_constant_pool] + enc.add4(RiscV.i_type(RiscV.OP_LOADFP, dst, 0b11, tmp, 0)); + } + @Override public RegMask killmap() { return new RegMask(riscv.T6); } + + // Delta is from opcode start. + @Override public byte encSize(int delta) { return 8; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short dst = (short)(enc.reg(this) - riscv.F_OFFSET); + short tmp = (short)riscv.T6; + // AUIPC dst,#hi20_constant_pool + enc.patch4(opStart , RiscV.u_type(RiscV.OP_AUIPC, tmp, delta>>12)); + // Load dst,[dst+#low12_constant_pool] + enc.patch4(opStart+4, RiscV.i_type(RiscV.OP_LOADFP, dst, 0b11, tmp, delta & 0xFFF)); + } + + @Override public void asm(CodeGen code, SB sb) { + _con.print(sb.p(code.reg(this)).p(" = #")); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FunRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FunRISC.java new file mode 100644 index 00000000..d3d64b06 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FunRISC.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class FunRISC extends FunNode implements MachNode { + FunRISC(FunNode fun) {super(fun);} + @Override public void computeFrameAdjust(CodeGen code, int maxReg) { + super.computeFrameAdjust(code,maxReg); + if( _hasCalls ) // If non-leaf, pad to 16b + _frameAdjust = ((_frameAdjust+8) & -16); + } + @Override public void encoding( Encoding enc ) { + int sz = _frameAdjust; + if( sz == 0 ) return; // Skip if no frame adjust + if( sz >= 1L<<12 ) throw Utils.TODO(); + enc.add4(RiscV.i_type(RiscV.OP_IMM, riscv.RSP, 0, riscv.RSP, -sz & 0xFFF)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/I2F8RISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/I2F8RISC.java new file mode 100644 index 00000000..1b53ab0f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/I2F8RISC.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// fcvt.d.w +// Converts a 32-bit signed integer, in integer register rs1 into a double-precision floating-point number in floating-point register rd. +public class I2F8RISC extends MachConcreteNode implements MachNode { + I2F8RISC(Node i2f8) {super(i2f8);} + @Override public String op() { return "cvtf"; } + @Override public RegMask regmap(int i) { assert i==1; return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.FMASK; } + @Override public void encoding( Encoding enc ) { + short dst = (short)(enc.reg(this )-riscv.F_OFFSET); + short src1 = enc.reg(in(1)); + int body = RiscV.r_type(RiscV.OP_FP,dst,RiscV.RM.RNE.ordinal(),src1,0,0x69); + enc.add4(body); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p("(flt)").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ImmRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ImmRISC.java new file mode 100644 index 00000000..ee247c3e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ImmRISC.java @@ -0,0 +1,55 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +abstract public class ImmRISC extends MachConcreteNode implements MachNode { + public final int _imm12; + ImmRISC( Node add, int imm12, boolean pop ) { + super(add); + _imm12 = imm12; + // If pop, we are moving from an ideal Node to MachNode; the ideal Node + // inputs got copied here, but we are absorbing the immediate into the + // MachNode and so do not want the constant input. + if( pop ) _inputs.pop(); + // if NOT pop, we are the 2nd-half of a 2-part expansion, reset the + // input edges to no-control + else { _inputs.set(0,null); _inputs.setX(1,add); } + } + ImmRISC( Node add, int imm12 ) { this(add,imm12,true); } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { + // Allow cloning of the 2nd half of a 2-part constant; this might + // trigger cloning the first part also. + return in(1) instanceof LUI || in(1) instanceof AUIPC; + } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + int imm12 = (_imm12<<20)>>20; // Sign extend 12 bits + p.p("( ").n(in(1)); + return p.p(String.format(" %s #%d )",glabel(),imm12)); + } + + abstract int opcode(); + abstract int func3(); + + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src = enc.reg(in(1)); + enc.add4(RiscV.i_type(opcode(), dst, func3(), src, _imm12 & 0xFFF)); + } + + // General form: "addi rd = rs1 + imm" + @Override public void asm(CodeGen code, SB sb) { + int imm12 = (_imm12<<20)>>20; // Sign extend 12 bits + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" ").p(glabel()).p(" #").p(imm12); + if( in(1) instanceof LUI lui ) + sb.p(" // #").hex4((int)(((TypeInteger)lui._con).value()) + imm12); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/Int8RISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/Int8RISC.java new file mode 100644 index 00000000..dd4a02a3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/Int8RISC.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Special instruction for loading 8 byte constants from the constant pool. + +public class Int8RISC extends ConstantNode implements MachNode, RIPRelSize { + final String _ext; + Int8RISC( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { return "ld8"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public Int8RISC copy() { return new Int8RISC(this,_ext); } + + @Override public byte encSize(int delta) { return 8; } + + @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); + short dst = enc.reg(this); + short tmp = (short)riscv.T6; + enc.largeConstant(this,_con, 0, -1); + // AUIPC dst,#hi20_constant_pool + enc.add4(RiscV.u_type(RiscV.OP_AUIPC, tmp, 0)); + // Load dst,[dst+#low12_constant_pool] + enc.add4(RiscV.i_type(RiscV.OP_LOAD, dst, 0b11, tmp, 0)); + } + @Override public RegMask killmap() { return new RegMask(riscv.T6); } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short dst = enc.reg(this); + short tmp = (short)riscv.T6; + // AUIPC dst,#hi20_constant_pool + enc.patch4(opStart , RiscV.u_type(RiscV.OP_AUIPC, tmp, delta>>12)); + // Load dst,[dst+#low12_constant_pool] + enc.patch4(opStart+4, RiscV.i_type(RiscV.OP_LOAD, dst, 0b11, tmp, delta & 0xFFF)); + } + + @Override public void asm(CodeGen code, SB sb) { + _con.print(sb.p(code.reg(this)).p(" = #")); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/IntRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/IntRISC.java new file mode 100644 index 00000000..35d2bc65 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/IntRISC.java @@ -0,0 +1,31 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +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 com.seaofnodes.simple.util.SB; + +// 12-bit integer constant. Larger constants are made up in the instruction +// selection by adding with a LUI. +public class IntRISC extends ConstantNode implements MachNode { + IntRISC(ConstantNode con) { super(con); } + @Override public String op() { return "ldi"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public IntRISC copy() { return new IntRISC(this); } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this); + int val = _con==Type.NIL ? 0 : (int)(((TypeInteger)_con).value() & 0xFFF); + // Explicit truncation of larger immediates; this will sign-extend on + // load and this is handled during instruction selection. + enc.add4(RiscV.i_type(RiscV.OP_IMM, dst, 0, riscv.ZERO, val)); + } + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + _con.print(sb.p(reg).p(" = #")); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LUI.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LUI.java new file mode 100644 index 00000000..aedd8b47 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LUI.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +// Load upper 20bits. +public class LUI extends ConstantNode implements MachNode { + public LUI( int imm20 ) { + super(TypeInteger.constant(imm20)); + assert (imm20 & 0xFFF)==0; + } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public LUI copy() { return new LUI((int)((TypeInteger)_con).value()); } + @Override public String op() { return "lui"; } + @Override public void encoding( Encoding enc ) { + long x = ((TypeInteger)_con).value(); + int imm20 = (int)(x>>12) & 0xFFFFF; + short dst = enc.reg(this); + int lui = RiscV.u_type(RiscV.OP_LUI, dst, imm20); + enc.add4(lui); + } + + @Override public void asm(CodeGen code, SB sb) { + String reg = code.reg(this); + sb.p(reg).p(" = #").hex4((int)(((TypeInteger)_con).value())); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LoadRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LoadRISC.java new file mode 100644 index 00000000..e9f3ec02 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LoadRISC.java @@ -0,0 +1,65 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Load memory addressing on RISC +// Support imm, reg(direct), or reg+off(indirect) addressing +// Base = base - base pointer, offset is added to base +// idx = null +// off = off - imm12 added to base +public class LoadRISC extends MemOpRISC { + public LoadRISC(LoadNode ld, Node base, int off) { super(ld, base, off, null); } + @Override public String op() { return "ld" +_sz; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.MEM_MASK; } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short ptr = enc.reg(ptr()); + int op = dst >= riscv.F_OFFSET ? RiscV.OP_LOADFP : RiscV.OP_LOAD; + if( dst >= riscv.F_OFFSET ) dst -= riscv.F_OFFSET; + enc.add4(RiscV.i_type(op, dst, func3(), ptr, _off)); + } + @Override int func3() { + int func3 = -1; + // no unsigned flavour for store, so both signed and unsigned trigger the same + Type declType = _con; + if( declType == TypeInteger. I8 ) func3=0; // LB + if( declType == TypeInteger.I16 ) func3=1; // LH + if( declType == TypeInteger.I32 ) func3=2; // LW + if( declType == TypeInteger.BOT ) func3=3; // LD + if( declType == TypeInteger. U8 ) func3=4; // LBU + if( declType == TypeInteger.BOOL) func3=4; // LBU + if( declType == TypeInteger.U16 ) func3=5; // LHU + if( declType == TypeInteger.U32 ) func3=6; // LWU + if( declType instanceof TypeInteger ti ) { + if( -128 <= ti._min && ti._max < 128 ) func3 = 0; // LB + } + + // float + if( declType == TypeFloat.F32) func3 = 2; // fLW fSW + if( declType == TypeFloat.F64) func3 = 3; // fLD fSD + + if( declType instanceof TypeMemPtr) func3 = 3; // 8 byte pointers (pick ld) + if( declType instanceof TypeFunPtr) func3 = 3; // 8 byte function pointers + if( declType instanceof TypeConAry ary ) + func3 = switch( ary.log_size() ) { + case 0 -> 4; // LBU + case 1 -> 5; // LHU + case 2 -> 2; // LW + case 3 -> 3; // LD + default -> throw Utils.TODO("RISC-V constant-array load for "+declType); + }; + if( func3 == -1 ) throw Utils.TODO("RISC-V load for "+declType); + return func3; + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(","); + asm_address(code,sb); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MemOpRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MemOpRISC.java new file mode 100644 index 00000000..e0bbc219 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MemOpRISC.java @@ -0,0 +1,51 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + +public abstract class MemOpRISC extends MemOpNode implements MachNode { + final int _off; // Limit 12 bits + final char _sz; + final int _bytes; + MemOpRISC(MemOpNode mop, Node base, int off, Node val) { + super(mop,mop); + assert base._type instanceof TypeMemPtr; + _inputs.setX(2, base ); // Base can be an Add, is no longer raw object base + _inputs.setX(3, null); // Never an index + _inputs.setX(4, val ); + _off = off; + _bytes = mop instanceof StoreNode st ? st.storeSize() : 1<<_con.log_size(); + _sz = (char)('0'+_bytes); + } + + @Override public String label() { return op(); } + Node val() { return in(4); } // Only for stores + + @Override protected ExprPrinter<Node> _printMach(ExprPrinter<Node> p) { return p.p(".").p(_name); } + + @Override public Type compute() { throw new AssertionError("Do not call compute on "+getClass().getSimpleName()); } + @Override public Node idealize() { throw new AssertionError("Do not call idealize on "+getClass().getSimpleName()); } + + // func3 is based on load/store size and extend + abstract int func3(); + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { + // 0 - ctrl + // 1 - memory + if( i==2 ) return riscv.RMASK; // base + // 2 - index + if( i==4 ) return riscv.MEM_MASK; // value + throw Utils.TODO(); + } + + SB asm_address(CodeGen code, SB sb) { + sb.p("[").p(code.reg(ptr())).p("+"); + return sb.p(_off).p("]"); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulFRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulFRISC.java new file mode 100644 index 00000000..dacec2b5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulFRISC.java @@ -0,0 +1,18 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// fmul.d +public class MulFRISC extends MachConcreteNode implements MachNode { + MulFRISC(Node mulf) {super(mulf);} + @Override public String op() { return "mulf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return riscv.FMASK; } + @Override public RegMask outregmap() { return riscv.FMASK; } + @Override public void encoding( Encoding enc ) { riscv.rf_type(enc,this,RiscV.RM.RNE,9); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulRISC.java new file mode 100644 index 00000000..9cb9ace7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MulRISC.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// mulh signed multiply instruction(no-imm form) +public class MulRISC extends MachConcreteNode implements MachNode{ + public MulRISC(Node mul) {super(mul);} + @Override public String op() { return "mul"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,0,1); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NegRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NegRISC.java new file mode 100644 index 00000000..3070c56c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NegRISC.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class NegRISC extends MachConcreteNode implements MachNode { + public NegRISC( Node neg ) { super(neg); } + @Override public String op() { return "neg"; } + @Override public RegMask regmap(int i) { assert i==1; return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src = enc.reg(in(1)); + RiscV.r_type(RiscV.OP,dst,0,0,src,0x20); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = -").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NewRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NewRISC.java new file mode 100644 index 00000000..40d0359c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NewRISC.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.print.ASMPrinter; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class NewRISC extends NewNode implements MachNode, RIPRelSize { + // A pre-zeroed chunk of memory. + NewRISC( NewNode nnn ) { super(nnn); } + @Override public void encoding( Encoding enc ) { + // Generic external encoding; 2 ops. + enc.external(this,"calloc"); + // A1 is a caller-save, allowed to crush building external address + // auipc + enc.add4(RiscV.i_type(RiscV.OP_IMM , _arg2Reg , 0, riscv.ZERO, 1)); + enc.add4(RiscV.u_type(RiscV.OP_AUIPC, riscv.A2 , 0)); + enc.add4(RiscV.i_type(RiscV.OP_JALR , riscv.RPC, 0, riscv.A2, 0)); + } + + // Patch is for running "new" in a JIT. + // Delta is from opcode start + @Override public byte encSize(int delta) { return 4*3; } + + // Patch is for running "new" in a JIT. + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + // Negative patches are JIT emulator targets. + if( opStart+delta < 0 ) { + delta -= 4; // Offset from the AUIPC start + int imm20 = delta>>12; + if( ((delta>>11)&1)==1 ) imm20++; // Correct accidental sign extension + int imm12 = delta&0xFFF; + enc.patch4(opStart+4,RiscV.u_type(RiscV.OP_AUIPC, riscv.A2 , imm20)); + enc.patch4(opStart+8,RiscV.i_type(RiscV.OP_JALR , riscv.RPC, 0, riscv.A2, imm12)); + } else + throw Utils.TODO(); + } + + // General form: "alloc #bytes PC" + @Override public void asm(CodeGen code, SB sb) { + sb.p("\n"); // Multi-op trigger + sb.p("ldi ").p(code._mach.regs()[_arg2Reg]).p(" = #1\n"); + sb.p("auipc a2=#calloc\n"); + sb.p("call a2+#calloc"); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NotRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NotRISC.java new file mode 100644 index 00000000..6f499db1 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NotRISC.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.NotNode; +import com.seaofnodes.simple.util.SB; + +public class NotRISC extends MachConcreteNode implements MachNode { + public NotRISC(NotNode not) { super(not); } + @Override public String op() { return "not"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src = enc.reg(in(1)); + // SEQZ is unsigned: negative nonzero inputs must also produce false. + enc.add4(RiscV.i_type(RiscV.OP_IMM, dst, 3, src, 1)); + } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(this)); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrIRISC.java new file mode 100644 index 00000000..937595a3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrIRISC.java @@ -0,0 +1,13 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class OrIRISC extends ImmRISC { + public OrIRISC( Node and, int imm) { super(and,imm); } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() { return 6; } + @Override public String glabel() { return "|"; } + @Override public String op() { return "ori"; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrRISC.java new file mode 100644 index 00000000..a310a1f4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/OrRISC.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class OrRISC extends MachConcreteNode implements MachNode { + public OrRISC(Node or) { super(or); } + @Override public String op() { return "or"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,6,0); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ParmRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ParmRISC.java new file mode 100644 index 00000000..0f8db4e5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ParmRISC.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ParmNode; + +public class ParmRISC extends ParmNode implements MachNode { + final RegMask _rmask; + ParmRISC(ParmNode parm) { + super(parm); + _rmask = riscv.callInMask(fun().sig(),_idx,0); + } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return _rmask; } + @Override public void encoding( Encoding enc ) { } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ProjRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ProjRISC.java new file mode 100644 index 00000000..2f9ac469 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ProjRISC.java @@ -0,0 +1,15 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ProjNode; + +public class ProjRISC extends ProjNode implements MachNode { + ProjRISC(ProjNode p) { super(p); } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { + return ((MachNode)in(0)).outregmap(_idx); + } + @Override public void encoding( Encoding enc ) { } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/RetRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/RetRISC.java new file mode 100644 index 00000000..33143c6b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/RetRISC.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ReturnNode; +import com.seaofnodes.simple.util.Utils; + +public class RetRISC extends ReturnNode implements MachNode { + public RetRISC(ReturnNode ret, FunNode fun) { super(ret, fun); fun.setRet(this); } + @Override public void encoding( Encoding enc ) { + int sz = fun()._frameAdjust; + if( sz >= 1L<<12 ) throw Utils.TODO(); + if( sz != 0 ) + enc.add4(RiscV.i_type(RiscV.OP_IMM, riscv.RSP, 0, riscv.RSP, sz & 0xFFF)); + short rpc = enc.reg(rpc()); + enc.add4(RiscV.i_type(RiscV.OP_JALR, riscv.ZERO, 0, rpc, 0)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetFRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetFRISC.java new file mode 100644 index 00000000..d037dd5c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetFRISC.java @@ -0,0 +1,34 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.BoolNode; +import com.seaofnodes.simple.node.MachConcreteNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; + +// corresponds to feq.d = equal +// fle.d = less than or equal +// flt.d = less than +public class SetFRISC extends MachConcreteNode implements MachNode { + final String _bop; // One of <,<=,== + SetFRISC( BoolNode bool ) { + super(bool); + assert bool.isFloat(); + _bop = bool.op(); + } + @Override public String op() { return "set"+_bop; } + @Override public RegMask regmap(int i) { return riscv.FMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src1 = (short)(enc.reg(in(1))-riscv.F_OFFSET); + short src2 = (short)(enc.reg(in(2))-riscv.F_OFFSET); + int body = RiscV.r_type(RiscV.OP_FP,dst,RiscV.fsetop(_bop),src1,src2,0b1010001/*FCMP*/); + enc.add4(body); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))).p(" ").p(_bop).p(" ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetIRISC.java new file mode 100644 index 00000000..9bc86565 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetIRISC.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.node.Node; + +// corresponds to slti,sltiu +public class SetIRISC extends ImmRISC { + final boolean _unsigned; // slti vs sltiu + public SetIRISC( Node src, int imm12, boolean unsigned ) { + this(src,imm12,unsigned,true); + } + public SetIRISC( Node src, int imm12, boolean unsigned, boolean pop ) { + super(src,imm12,pop); + if( !pop ) _inputs.setLen(2); + _unsigned = unsigned; + } + @Override public String op() { return "slt" + (_unsigned ? "u":"") + "i"; } + @Override public String glabel() { return "<"+ (_unsigned ? "u":""); } + @Override int opcode() { return 0b0010011; } + @Override int func3() { return _unsigned ? 0b011 : 0b010; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetRISC.java new file mode 100644 index 00000000..848cc59f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SetRISC.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// corresponds to slt sltu +public class SetRISC extends MachConcreteNode implements MachNode { + final boolean _unsigned; + public SetRISC( Node cmp, boolean unsigned ) { super(cmp); _unsigned=unsigned; } + @Override public String op() { return "slt" + (_unsigned ? "u":""); } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { + riscv.r_type(enc,this,_unsigned ? 0b011 : 0b010,0); + } + @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))); + } +}
\ No newline at end of file diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllIRISC.java new file mode 100644 index 00000000..1425caa2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllIRISC.java @@ -0,0 +1,13 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class SllIRISC extends ImmRISC { + public SllIRISC( Node and, int imm) { super(and,imm); } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() { return 1; } + @Override public String glabel() { return "<<"; } + @Override public String op() { return "slli"; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllRISC.java new file mode 100644 index 00000000..5a4d4cc6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SllRISC.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Shift left logical +public class SllRISC extends MachConcreteNode implements MachNode { + public SllRISC(Node sll) { super(sll); } + @Override public String op() { return "shl"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,1,0); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java new file mode 100644 index 00000000..d455337b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java @@ -0,0 +1,61 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.SplitNode; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class SplitRISC extends SplitNode { + SplitRISC( LRG lrg, String kind, byte round ) { super(lrg, kind,round, new Node[2]); } + @Override public RegMask regmap(int i) { return riscv.SPLIT_MASK; } + @Override public RegMask outregmap() { return riscv.SPLIT_MASK; } + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this ); + short src = enc.reg(in(1)); + + // Flag for being either XMM or stack + boolean dstX = dst >= riscv.F_OFFSET; + boolean srcX = src >= riscv.F_OFFSET; + + if( dst >= riscv.MAX_REG ) { + // Store to SP + if( src >= riscv.MAX_REG ) { + throw Utils.TODO(); // Very rare stack-stack move + } + int off = fun(enc).computeStackOffset(enc._code,dst); + int op = srcX ? RiscV.OP_STOREFP : RiscV.OP_STORE; + if( srcX ) src -= riscv.F_OFFSET; + enc.add4(RiscV.s_type(op, 0b011, riscv.RSP, src, off)); + return; + } + if( src >= riscv.MAX_REG ) { + // Load from SP + int off = fun(enc).computeStackOffset(enc._code,src); + int op = dstX ? RiscV.OP_LOADFP : RiscV.OP_LOAD; + if( dstX ) dst -= riscv.F_OFFSET; + enc.add4(RiscV.i_type(op, dst, 0b011, riscv.RSP, off)); + return; + } + // pick opcode based on regs + if( !dstX && !srcX ) { + // GPR->GPR + enc.add4(RiscV.r_type(RiscV.OP,dst,0,src,riscv.ZERO,0)); + } else if( dstX && srcX ) { + // FPR->FPR + src -= riscv.F_OFFSET; + dst -= riscv.F_OFFSET; + enc.add4(RiscV.r_type(RiscV.OP_FP, dst, 0b000, src, src, 0b0010101)); // dst = FMIN(src,src) + } else if(!srcX && dstX) { + //GPR->FPR + // fmv.d.x + enc.add4(RiscV.r_type(RiscV.OP_FP, dst - riscv.F_OFFSET, 0, src, 0, 0b1111001)); + } else if(srcX && !dstX) { + //FPR->GPR + //fmv.x.d + enc.add4(RiscV.r_type(RiscV.OP_FP, dst, 0, src - riscv.F_OFFSET, 0, 0b1110001)); + } + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraIRISC.java new file mode 100644 index 00000000..b316b303 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraIRISC.java @@ -0,0 +1,15 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class SraIRISC extends ImmRISC { + public SraIRISC( Node and, int imm) { + super(and,imm | (0x20 << 5)); + } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() { return 5;} + @Override public String glabel() { return ">>"; } + @Override public String op() { return "srai"; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraRISC.java new file mode 100644 index 00000000..e2e34415 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SraRISC.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Right Shift Arithmetic +public class SraRISC extends MachConcreteNode implements MachNode { + public SraRISC(Node sra) { super(sra); } + @Override public String op() { return "sar"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,0b101,0b00100000); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlIRISC.java new file mode 100644 index 00000000..d0ea6b18 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlIRISC.java @@ -0,0 +1,13 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class SrlIRISC extends ImmRISC { + public SrlIRISC( Node and, int imm, boolean pop) { super(and,imm,pop); } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() { return 5; } + @Override public String glabel() { return ">>>"; } + @Override public String op() { return "srli"; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlRISC.java new file mode 100644 index 00000000..1eccc1c8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SrlRISC.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Right Shift Logical +public class SrlRISC extends MachConcreteNode implements MachNode { + public SrlRISC(Node srli) { super(srli); } + @Override public String op() { return "shr"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,7,0); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java new file mode 100644 index 00000000..c3fcf4d3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java @@ -0,0 +1,56 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Store memory addressing on ARM +// Support imm, reg(direct), or reg+off(indirect) addressing +// Base = base - base pointer, offset is added to base +// idx = null +// off = off - offset added to base) +// imm = imm - immediate value to store +// val = Node of immediate value to store(null if its a constant immediate) + +//e.g s.cs[0] = 67; // C +// base = s.cs, off = 4, imm = 67, val = null + +// sw rs2,offset(rs1) +public class StoreRISC extends MemOpRISC { + StoreRISC( StoreNode st, Node base, int off, Node val ) { super(st, base, off, val); } + @Override public String op() { return "st"+_sz; } + @Override public RegMask regmap(int i) { + // 0 - ctrl + if( i==1 ) return null; // mem + if( i==2 ) return riscv.RMASK; // ptr + // 2 - index + if( i==4 ) return _bytes >= 4 + ? riscv.MEM_MASK // Word/dword value in GPR or FPR + : riscv.RMASK; // Byte/short stores require a GPR + return null; // Anti-dependence + } + @Override public RegMask outregmap() { return null; } + + @Override public void encoding( Encoding enc ) { + short val = enc.reg(val()); + short ptr = enc.reg(ptr()); + int op = val >= riscv.F_OFFSET ? RiscV.OP_STOREFP : RiscV.OP_STORE; + if( val >= riscv.F_OFFSET ) val -= riscv.F_OFFSET; + enc.add4(RiscV.s_type(op, func3()&7, ptr, val == -1 ? riscv.ZERO : val, _off)); + } + + // func3 is based on load/store size and extend + @Override int func3() { + return Integer.numberOfTrailingZeros(_bytes); + } + + @Override public void asm(CodeGen code, SB sb) { + asm_address(code,sb).p(","); + if( val()==null ) sb.p("#").p("0"); + else sb.p(code.reg(val())); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubFRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubFRISC.java new file mode 100644 index 00000000..044820cf --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubFRISC.java @@ -0,0 +1,18 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// fsub.d +public class SubFRISC extends MachConcreteNode implements MachNode { + SubFRISC(Node subf) {super(subf);} + @Override public String op() { return "subf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return riscv.FMASK; } + @Override public RegMask outregmap() { return riscv.FMASK; } + @Override public void encoding( Encoding enc ) { riscv.rf_type(enc,this,RiscV.RM.RNE,5); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubRISC.java new file mode 100644 index 00000000..2adfce23 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SubRISC.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class SubRISC extends MachConcreteNode implements MachNode { + public SubRISC( Node sub ) { super(sub); } + @Override public String op() { return "sub"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,0,0x20); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TFPRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TFPRISC.java new file mode 100644 index 00000000..44ef1a95 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TFPRISC.java @@ -0,0 +1,56 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class TFPRISC extends FunPtrNode implements MachNode, RIPRelSize { + final String _ext; + // Pointer to a Simple function + TFPRISC( FunPtrNode fptr ) { super(fptr); _ext = null; } + // Pointer to an Extern "C" function + TFPRISC( TypeFunPtr fptr, String ext ) { super(fptr,CodeGen.CODE._start,null); _type = fptr; _ext = ext; } + @Override public String op() { return "ldx"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public TFPRISC copy() { return new TFPRISC((TypeFunPtr)_con,_ext); } + @Override public void encoding( Encoding enc ) { + if( _ext==null ) enc.relo(this); // Internal patch + else enc.external(this,_ext); // ELF-file external patch + // TODO: 1 op encoding, plus a TODO if it does not fit + short dst = enc.reg(this); + // auipc t0,0 + enc.add4(RiscV.u_type(RiscV.OP_AUIPC, dst, 0)); + // addi t1,t0 + #0 + enc.add4(RiscV.i_type(RiscV.OP_IMM, dst, 0, dst, 0)); + } + + @Override public byte encSize(int delta) { return 8; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short rpc = enc.reg(this); + if( opLen==8 ) { + int hi20 = (delta + 0x800) >> 12; + int lo12 = delta - (hi20 << 12); + // AUIPC (upper 20 bits) + // opstart of add + enc.patch4(opStart ,RiscV.u_type(RiscV.OP_AUIPC, rpc, hi20)); + // addi(low 12 bits) + enc.patch4(opStart+4,RiscV.i_type(RiscV.OP_IMM, rpc, 0, rpc, lo12 & 0xFFF)); + // addi + } else { + // should not happen as one instruction is 4 byte, and TFP arm encodes 2. + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + _type.print(sb.p(code.reg(this)).p(" #")); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java new file mode 100644 index 00000000..a5ff3753 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeMemPtr; +import com.seaofnodes.simple.util.SB; + +public class TMPRISC extends ConstantNode implements MachNode, RIPRelSize { + final String _ext; + TMPRISC( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { return "ldp"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public boolean isClone() { return true; } + @Override public TMPRISC copy() { return new TMPRISC(this,_ext); } + @Override public void encoding( Encoding enc ) { + if( _ext!=null ) enc.externalData(this,_ext); + else enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1); + short dst = enc.reg(this); + // AUIPC dst,#hi20_constant_pool + enc.add4(RiscV.u_type(RiscV.OP_AUIPC, dst, 0)); + // addi dst,[dst+#low12_constant_pool] + enc.add4(RiscV.i_type(RiscV.OP_IMM, dst, 0, dst, 0)); + } + + @Override public byte encSize(int delta) { return 8; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + short dst = enc.reg(this); + // AUIPC dst,#hi20_constant_pool + enc.patch4(opStart , RiscV.u_type(RiscV.OP_AUIPC, dst, (delta+0x800)>>12)); + // ADDI's low 12 bits are signed; round the AUIPC page accordingly. + enc.patch4(opStart+4, RiscV.i_type(RiscV.OP_IMM, dst, 0, dst, delta & 0xFFF)); + } + + @Override public void asm(CodeGen code, SB sb) { + _con.print(sb.p(code.reg(this)).p(" #")); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/UJmpRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/UJmpRISC.java new file mode 100644 index 00000000..448d9ec3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/UJmpRISC.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// unconditional jump +public class UJmpRISC extends CFGNode implements MachNode, RIPRelSize { + @Override public String op() { return "jmp"; } + @Override public String label() { return op(); } + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p("jmp "); } + @Override public RegMask regmap(int i) {return null; } + @Override public RegMask outregmap() { return null; } + @Override public Type compute() { throw Utils.TODO(); } + @Override public Node idealize() { throw Utils.TODO(); } + @Override public void encoding( Encoding enc ) { + // Short form +/-4K: beq r0,r0,imm12 + // Long form: auipc rX,imm20/32; jal r0,[rX+imm12/32] + enc.jump(this,uctrl()); + enc.add4(RiscV.j_type(RiscV.OP_JAL, 0, 0)); + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { + if( -(1L<<20) <= delta && delta < (1L<<20) ) return 4; + // 2 word encoding needs a tmp register, must teach RA + throw Utils.TODO(); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + if( opLen==4 ) { + enc.patch4(opStart,RiscV.j_type(RiscV.OP_JAL, 0, delta)); + } else { + throw Utils.TODO(); + } + } + + @Override public void asm(CodeGen code, SB sb) { + CFGNode target = uctrl(); + //assert target.nOuts() > 1; // Should optimize jmp to empty targets + sb.p(label(target)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorIRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorIRISC.java new file mode 100644 index 00000000..f4323cae --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorIRISC.java @@ -0,0 +1,14 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class XorIRISC extends ImmRISC { + public XorIRISC( Node and, int imm) { super(and,imm); } + public XorIRISC( Node and, int imm, boolean pop) { super(and,imm,pop); } + @Override int opcode() { return RiscV.OP_IMM; } + @Override int func3() { return 4; } + @Override public String glabel() { return "^"; } + @Override public String op() { return "xori"; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorRISC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorRISC.java new file mode 100644 index 00000000..413a7c3b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/XorRISC.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class XorRISC extends MachConcreteNode implements MachNode { + public XorRISC(Node or) { super(or); } + @Override public String op() { return "xor"; } + @Override public RegMask regmap(int i) { return riscv.RMASK; } + @Override public RegMask outregmap() { return riscv.WMASK; } + @Override public void encoding( Encoding enc ) { riscv.r_type(enc,this,4,0); } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java new file mode 100644 index 00000000..0195542c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java @@ -0,0 +1,450 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import static com.seaofnodes.isa.RiscV.*; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +public class riscv extends Machine { + + public riscv(CodeGen code) { + if( !"SystemV".equals(code._callingConv) ) + throw new IllegalArgumentException("Unknown calling convention "+code._callingConv); + } + + @Override public String name() {return "riscv";} + + // Using ABI names instead of register names + public static int ZERO = 0, RPC= 1, RSP= 2, S12= 3, S13= 4, T0 = 5, T1 = 6, T2 = 7; + public static int S0 = 8, S1 = 9, A0 = 10, A1 = 11, A2 = 12, A3 = 13, A4 = 14, A5 = 15; + public static int A6 = 16, A7 = 17, S2 = 18, S3 = 19, S4 = 20, S5 = 21, S6 = 22, S7 = 23; + public static int S8 = 24, S9 = 25, S10= 26, S11= 27, T3 = 28, T4 = 29, T5 = 30, T6 = 31; + + // FP registers + public static int F0 = 32, F1 = 33, F2 = 34, F3 = 35, F4 = 36, F5 = 37, F6 = 38, F7 = 39; + public static int FS0 = 40, FS1 = 41, FA0 = 42, FA1 = 43, FA2 = 44, FA3 = 45, FA4 = 46, FA5 = 47; + public static int FA6 = 48, FA7 = 49, FS2 = 50, FS3 = 51, FS4 = 52, FS5 = 53, FS6 = 54, FS7 = 55; + public static int FS8 = 56, FS9 = 57, FS10 = 58, FS11 = 59, FT8 = 60, FT9 = 61, FT10 = 62, FT11 = 63; + + static final int MAX_REG = 64; + + public static final int F_OFFSET = 32; + + static final String[] REGS = new String[] { + //"zero","rpc" , "rsp" , "s12" , "s13" , "t0" , "t1" , "t2" , + //"s0" , "s1" , "a0" , "a1" , "a2" , "a3" , "a4" , "a5" , + //"a6" , "a7" , "s2" , "s3" , "s4" , "s5" , "s6" , "s7" , + //"s8" , "s9" , "s10" , "s11" , "t3" , "t4" , "t5" , "t6" , + //"f0" , "f1" , "f2" , "f3" , "f4" , "f5" , "f6" , "f7" , + //"fs0" , "fs1" , "fa0" , "fa1" , "fa2" , "fa3" , "fa4" , "fa5" , + //"fa6" , "fa7" , "fs2" , "fs3" , "fs4" , "fs5" , "fs6" , "fs7" , + //"fs8" , "fs9" , "fs10", "fs11", "ft8" , "ft9" , "ft10", "ft11" + // Register-based naming hella easier to debug than "official" register + // names when single-stepping in debugger. + "zero", "r1" , "rsp" , "r3" , "r4" , "r5" , "r6" , "r7" , + "r8" , "r9" , "r10" , "r11", "r12", "r13", "r14", "r15", + "r16" , "r17" , "r18" , "r19", "r20", "r21", "r22", "r23", + "r24" , "r25" , "r26" , "r27", "r28", "r29", "r30", "r31", + "f0" , "f1" , "f2" , "f3" , "f4" , "f5" , "f6" , "f7" , + "fs0" , "fs1" , "fa0" , "fa1" , "fa2" , "fa3" , "fa4" , "fa5" , + "fa6" , "fa7" , "fs2" , "fs3" , "fs4" , "fs5" , "fs6" , "fs7" , + "fs8" , "fs9" , "fs10", "fs11", "ft8" , "ft9" , "ft10", "ft11" + }; + @Override public String[] regs() { return REGS; } + + // General purpose register mask: pointers and ints, not floats + static final long RD_BITS = 0b11111111111111111111111111111111L; // All the GPRs + static final RegMask RMASK = new RegMask(RD_BITS); + + static final long WR_BITS = 0b11111111111111111111111111111010L; // All the GPRs, minus ZERO and RSP + static final RegMask WMASK = new RegMask(WR_BITS); + // Float mask from(f0-ft10). TODO: ft10,ft11 needs a larger RegMask + static final long FP_BITS = 0b11111111111111111111111111111111L<<F0; + static final RegMask FMASK = new RegMask(FP_BITS); + + // Load/store mask; both GPR and FPR + static final RegMask MEM_MASK = new RegMask(WR_BITS | FP_BITS); + + static final RegMask SPLIT_MASK = new RegMask( WR_BITS | FP_BITS, -1L ); + + // Arguments masks + public static RegMask A0_MASK = new RegMask(A0); + public static RegMask A1_MASK = new RegMask(A1); + public static RegMask A2_MASK = new RegMask(A2); + public static RegMask A3_MASK = new RegMask(A3); + public static RegMask A4_MASK = new RegMask(A4); + public static RegMask A5_MASK = new RegMask(A5); + public static RegMask A6_MASK = new RegMask(A6); + public static RegMask A7_MASK = new RegMask(A7); + + public static final RegMask RPC_MASK = new RegMask(RPC); + + // Float arguments masks + public static RegMask FA0_MASK = new RegMask(FA0); + public static RegMask FA1_MASK = new RegMask(FA1); + public static RegMask FA2_MASK = new RegMask(FA2); + public static RegMask FA3_MASK = new RegMask(FA3); + public static RegMask FA4_MASK = new RegMask(FA4); + public static RegMask FA5_MASK = new RegMask(FA5); + public static RegMask FA6_MASK = new RegMask(FA6); + public static RegMask FA7_MASK = new RegMask(FA7); + + // Int/ptr arguments calling convention + static RegMask[] CALLINMASK = new RegMask[] { + A0_MASK, + A1_MASK, + A2_MASK, + A3_MASK, + A4_MASK, + A5_MASK, + A6_MASK, + A7_MASK + }; + + static RegMask[] XMMS = new RegMask[] { + FA0_MASK, + FA1_MASK, + FA2_MASK, + FA3_MASK, + FA4_MASK, + FA5_MASK, + FA6_MASK, + FA7_MASK + }; + + // Since riscv instructions are fixed we can just or them together + + public static void r_type(Encoding enc, Node n, int func3, int func7) { + short dst = enc.reg(n); + short src1 = enc.reg(n.in(1)); + short src2 = enc.reg(n.in(2)); + int body = RiscV.r_type(OP,dst,func3,src1,src2,func7); + enc.add4(body); + } + + public static void rf_type(Encoding enc, Node n, RM func3, int func7) { + short dst = (short)(enc.reg(n )-F_OFFSET); + short src1 = (short)(enc.reg(n.in(1))-F_OFFSET); + short src2 = (short)(enc.reg(n.in(2))-F_OFFSET); + int body = RiscV.r_type(OP_FP,dst,func3.ordinal(),src1,src2,func7); + enc.add4(body); + } + + @Override public RegMask callArgMask( TypeFunPtr tfp, int idx, int maxArgSlot ) { return callInMask(tfp,idx,maxArgSlot); } + static RegMask callInMask( TypeFunPtr tfp, int idx, int maxArgSlot ) { + if( idx==0 ) return RPC_MASK; + if( idx==1 ) return null; + if( idx-2 >= tfp.nargs() ) return null; // Anti-dependence + boolean hidden = hiddenSelf(tfp); + int sigidx = idx-2; + if( hidden ) { + if( sigidx==0 ) return null; + sigidx--; + } + // Count floats in signature up to index + int fcnt=0; + for( int i=hidden ? 1 : 0; i<idx-2; i++ ) + if( tfp.arg(i) instanceof TypeFloat ) + fcnt++; + // Floats up to XMMS in XMM registers + if( tfp.arg(idx-2) instanceof TypeFloat ) { + if( fcnt < XMMS.length ) + return XMMS[fcnt]; + } else { + RegMask[] cargs = CALLINMASK; + if( sigidx-fcnt < cargs.length ) + return cargs[sigidx-fcnt]; + } + // Pass on stack slot(8 and higher) + if( maxArgSlot>0 ) throw Utils.TODO(); + return new RegMask(MAX_REG + 1 + sigidx); + } + + @Override public short maxArgSlot( TypeFunPtr tfp ) { + int icnt=0, fcnt=0; // Count of ints, floats + for( int i=hiddenSelf(tfp) ? 1 : 0; i<tfp.nargs(); i++ ) { + if( tfp.arg(i) instanceof TypeFloat ) fcnt++; + else icnt++; + } + int nstk = Math.max(icnt-8,0)+Math.max(fcnt-8,0); + return (short)nstk; + } + private static boolean hiddenSelf(TypeFunPtr tfp) { + return tfp.nargs() > 0 && tfp.arg(0) == TypePtr.PTR; + } + + static final long CALLER_SAVE = + (1L<<RPC) | + (1L<<A0 ) | (1L<<A1 ) | (1L<<A2 ) | (1L<<A3 ) | (1L<<A4 ) | (1L<<A5 ) | (1L<<A6 ) | (1L<<A7 ) | + (1L<<T0 ) | (1L<<T1 ) | (1L<<T2 ) | (1L<<T3 ) | (1L<<T4 ) | (1L<<T5 ) | (1L<<T6 ) | + (1L<<FA0) | (1L<<FA1) | (1L<<FA2) | (1L<<FA3) | (1L<<FA4) | (1L<<FA5) | (1L<<FA6) | (1L<<FA7) | + (1L<<F0 ) | (1L<<F1 ) | (1L<<F2 ) | (1L<<F3 ) | (1L<<F4 ) | (1L<<F5 ) | (1L<<F6 ) | (1L<<F7 ) | + (1L<<FT8) | (1L<<FT9) | (1L<<FT10)| (1L<<FT11); + + @Override public long callerSave() { return CALLER_SAVE; } + @Override public long neverSave() { return (1L<<RSP) | (1L<<ZERO); } + @Override public RegMask retMask( TypeFunPtr tfp ) { + return tfp.ret() instanceof TypeFloat ? FA0_MASK : A0_MASK; + } + @Override public int rpc() { return RPC; } + + // Return a MachNode unconditional branch + @Override public CFGNode jump() { return new UJmpRISC(); } + + // Create a split op; any register to any register, including stack slots + @Override public SplitNode split(LRG lrg, String kind, byte round ) { return new SplitRISC(lrg, kind,round); } + + // True if signed 12-bit immediate + public static boolean imm12(TypeInteger ti) { + // 52 = 64-12 + return ti.isConstant() && ((ti.value()<<52)>>52) == ti.value(); + } + // True if HIGH 20-bit signed immediate, with all zeros low. + public static boolean imm20Exact(TypeInteger ti) { + // shift left 32 to clear out the upper 32 bits. + // shift right SIGNED to sign-extend upper 32 bits; then shift 12 more to clear out lower 12 bits. + // shift left 12 to re-center the bits. + return ti.isConstant() && (((ti.value()<<32)>>44)<<12) == ti.value(); + } + + @Override public Node instSelect( Node n ) { + return switch (n) { + case AddNode add -> add(add); + case AndNode and -> and(and); + case BoolNode bool -> cmp(bool); + case CallNode call -> call(call); + case PtrToIntNode ptr -> new PtrToIntMach(ptr); + case GuardNode guard -> new GuardMach(guard); + case CallEndNode cend -> new CallEndMach(cend); + case CLInitNode init -> new CLInitNode(init); + case CProjNode c -> new CProjNode(c); + case ConstantNode con -> con(con); + case DivNode div -> div.mode()==2 ? new DivFRISC(div) : new DivRISC(div); + case EscapeNode esc -> new EscapeNode(esc); + case FunNode fun -> new FunRISC(fun); + case FunPtrNode fptr -> fptr(fptr); + case IfNode iff -> jmp(iff); + case LoadNode ld -> ld(ld); + case MemMergeNode mem -> new MemMergeNode(mem); + case MinusNode neg -> { + if( neg.mode()==2 ) throw Utils.TODO(); + yield new NegRISC(neg); + } + case MulNode mul -> mul.mode()==2 ? new MulFRISC(mul) : new MulRISC(mul); + case NewNode nnn -> nnn(nnn); + case NotNode not -> new NotRISC(not); + case OrNode or -> or(or); + case ParmNode parm -> new ParmRISC(parm); + case BulkMemPhiNode phi-> new BulkMemPhiNode(phi); + case MemPhiNode phi -> new MemPhiNode(phi); + case PhiNode phi -> new PhiNode(phi); + case ProjNode prj -> prj(prj); + case ReadOnlyNode read-> new ReadOnlyMach(read); + case ReturnNode ret -> new RetRISC(ret, ret.fun()); + case SarNode sar -> sra(sar); + case ShlNode shl -> sll(shl); + case ShrNode shr -> srl(shr); + case StartCUNode start -> new StartCUNode(start); + case StopCUNode stop -> new StopCUNode(stop); + case StartNode start -> new StartNode(start); + case StopNode stop -> new StopNode(stop); + case StoreNode st -> st(st); + case SubNode sub -> sub.mode()==2 ? new SubFRISC(sub) : sub(sub); + case ToFloatNode tfn -> i2f8(tfn); + case ToIntegerNode cvt -> new F8ToIRISC(cvt); + case XorNode xor -> xor(xor); + + case LoopNode loop -> new LoopNode(loop); + case RegionNode region -> new RegionNode(region); + default -> throw Utils.TODO(); + }; + } + + private Node add(AddNode add) { + if( add.mode()==2 ) + return new AddFRISC(add); + if( add.in(2) instanceof ConstantNode off2 && off2._con instanceof TypeInteger ti && imm12(ti) ) + return new AddIRISC(add, (int)ti.value(),true); + return new AddRISC(add); + } + + private Node and(AndNode and) { + if( and.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti ) { + if( imm12(ti) ) + return new AndIRISC(and, (int)ti.value()); + // Could be any size low bit mask + if( ti.value() == 0xFFFFFFFFL ) + return new SrlIRISC(new SllIRISC(and,32),32,false); + } + return new AndRISC(and); + } + + private Node call(CallNode call) { + return call.fptr() instanceof FunPtrNode fptr + ? new CallRISC(call, (TypeFunPtr)fptr._type) + : call.fptr() instanceof ConstantNode con + ? new CallRISC(call, (TypeFunPtr)con._type) + : new CallRRISC(call); + } + private Node nnn(NewNode nnn) { + // TODO: pass in the TFP for alloc + return new NewRISC(nnn); + } + + private Node cmp(BoolNode bool) { + // Float variant directly implemented in hardware + if( bool.isFloat() ) + return new SetFRISC(bool); + + // Only < and <u are implemented in hardware. + // x < y - as-is + // x <= y - flip and negate; !(y < x); `slt tmp=y,x; xori dst=tmp,#1` + // x == y - sub and vs0 == `sub tmp=x-y; sltu dst=tmp,#1` + + // x > y - swap; y < x + // x >= y - swap and negate; !(x < y); `slt tmp=y,x;` then NOT. + // x != y - sub and vs0 == `sub tmp=x-y; sltu dst=tmp,#1` then NOT. + + // The ">", ">=" and "!=" in Simple include a NotNode, which can be + // implemented with a XOR. If one of the above is followed by a NOT + // we can remove the double XOR in the encodings. + + return switch( bool.op() ) { + case "<" -> bool.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti) + ? new SetIRISC(bool, (int)ti.value(),false) + : new SetRISC(bool, false); + // x <= y - flip and negate; !(y < x); `slt tmp=y,x; xori dst=tmp,#1` + case "<=" -> new XorIRISC(new SetRISC(bool.swap12(), false),1,false); + // x == y - sub and vs0 == `sub tmp=x-y; sltu dst=tmp,#1` + case "==" -> new SetIRISC(new SubRISC(bool),1,true,false); + default -> throw Utils.TODO(); + }; + } + + private Node con( ConstantNode con ) { + if( !con._con.isConstant() ) + return ConstantNode.raw(con); // Default unknown caller inputs + String ext = con instanceof ExternNode ext0 ? ext0._extern : null; + return switch( con._con ) { + case TypeInteger ti -> { + // External; without knowing the size assume we have to load something + if( ext!=null ) yield new Int8RISC(con,ext); + if( imm12(ti) ) yield new IntRISC(con); + long x = ti.value(); + if( imm20Exact(ti) ) yield new LUI((int)x); + if( (x<<32)>>32 == x ) { // Signed lower 32-bit immediate + // Here, the low 12 bits get sign-extended, which means if + // bit11 is set, the value is negative and lowers the LUI + // value. Add a bit 12 to compensate + if( ((x>>11)&1)==1 ) x += 0x1000; + yield new AddIRISC(new LUI((int)(x & ~0xFFF)), (int)(x & 0xFFF),false); + } + // Need more complex sequence for larger constants... or a load + // from a constant pool, which does not need an extra register + yield new Int8RISC(con,null); + } + // Load from constant pool + case TypeFloat tf -> new FltRISC(con,ext); + case TypeMemPtr tmp -> new TMPRISC(con,ext); + case TypeFunPtr tfp -> new TFPRISC(tfp,ext); + case TypeNil tn -> throw Utils.TODO(); + // TOP, BOTTOM, XCtrl, Ctrl, etc. Never any executable code. + case Type t -> t==Type.NIL ? new IntRISC(con) : ConstantNode.raw(con); + }; + } + + private Node fptr( FunPtrNode con ) { + return new TFPRISC(con); + } + + private Node jmp( IfNode iff ) { + if( iff.in(1) instanceof BoolNode bool && !bool.isFloat() ) { + // if less than or equal switch inputs + String bop = bool.op(); + if( bop.equals("<=") || bop.equals(">") ) + return new BranchRISC(iff, IfNode.swap(bop), bool.in(2), bool.in(1)); + return new BranchRISC(iff, bop, bool.in(1), bool.in(2)); + } + // Vs zero + return new BranchRISC(iff, iff.in(1)==null ? "==" : "!=", iff.in(1), null); + } + + private Node or(OrNode or) { + if( or.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti)) + return new OrIRISC(or, (int)ti.value()); + return new OrRISC(or); + } + + private Node xor(XorNode xor) { + if( xor.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti)) + return new XorIRISC(xor, (int)ti.value()); + return new XorRISC(xor); + } + + private Node sra(SarNode sar) { + if( sar.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti)) + return new SraIRISC(sar, (int)ti.value()); + return new SraRISC(sar); + } + + private Node srl(ShrNode shr) { + if( shr.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti)) + return new SrlIRISC(shr, (int)ti.value(),true); + return new SrlRISC(shr); + } + + private Node sll(ShlNode sll) { + if( sll.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti)) + return new SllIRISC(sll, (int)ti.value()); + return new SllRISC(sll); + } + + private Node sub(SubNode sub) { + return sub.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti) + ? new AddIRISC(sub, (int)(-ti.value()),true) + : new SubRISC(sub); + } + + private Node i2f8(ToFloatNode tfn) { + assert tfn.in(1)._type instanceof TypeInteger ti; + return new I2F8RISC(tfn); + } + + private Node prj(ProjNode prj) { + if( prj.in(0) instanceof CLInitNode ) return new ProjNode(prj); + TypeInteger ret = cReturn(prj); + if( ret!=null ) return new ExtendRISC(prj,ret); + return prj.in(0) instanceof StartNode ? new ProjNode(prj) : new ProjRISC(prj); + } + + private Node ld(LoadNode ld) { + return new LoadRISC(ld,address(ld),off); + } + + private Node st(StoreNode st) { + Node xval = st.val() instanceof ConstantNode con && con._con == TypeInteger.ZERO ? null : st.val(); + return new StoreRISC(st,address(st), off, xval); + } + + // Gather 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; + private Node address( MemOpNode mop ) { + off = 0; // Reset + 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 ConstantNode con && con._con instanceof TypeInteger ti && imm12(ti) ) { + off = (int)ti.value(); + } else { + base = new AddRISC(base,mop.off()); + base._type = mop.ptr()._type; + } + return base; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java new file mode 100644 index 00000000..74af9266 --- /dev/null +++ b/chapter-modules/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.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class AddFMemX86 extends MemOpX86 { + AddFMemX86( AddNode add, LoadNode ld , Node base, Node idx, int off, int scale, Node val ) { + super(add,ld, base, idx, off, scale, 0, val ); + } + @Override public String op() { return "addf"+_sz; } + @Override public RegMask regmap(int i) { + if( i==1 ) return null; // 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(); + } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + @Override public int twoAddress() { return 4; } + @Override public void encoding( Encoding enc ) { + X86.memory(enc,0xF2,0x0F58,false,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(ptr()),enc.reg(idx()),_off,_scale); + } + + // 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/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFX86.java new file mode 100644 index 00000000..c7d0cf9c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class AddFX86 extends MachConcreteNode implements MachNode { + AddFX86( Node addf ) { super(addf); } + @Override public String op() { return "addf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + @Override public int twoAddress() { return 1; } + @Override public boolean commutes() { return true; } + + @Override public void encoding( Encoding enc ) { + X86.sse(enc,0xF2,0x0F58,false,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(in(2))-x86_64_v2.XMM_OFFSET); + } + + // General form: "addf dst += src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" += ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddIX86.java new file mode 100644 index 00000000..0e652a93 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddIX86.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class AddIX86 extends ImmX86 { + AddIX86( Node add, int imm ) { super(add,imm); } + @Override public String op() { + return _imm == 1 ? "inc" : (_imm == -1 ? "dec" : "addi"); + } + @Override public String glabel() { return "+"; } + @Override int opcode() { return 0x81; } + @Override int mod() { return 0; } + @Override public final void encoding( Encoding enc ) { + if( _imm== 1 || _imm== -1 ) { + short dst = enc.reg(this); // Also src1 + X86.unary(enc,0xFF,_imm==1 ? 0 : 1,dst); + } else super.encoding(enc); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddMemX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddMemX86.java new file mode 100644 index 00000000..ed65b0a8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddMemX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +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; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public int twoAddress() { return 4; } + @Override public void encoding( Encoding enc ) { + X86.memory(enc,0,0x03,true,enc.reg(this),enc.reg(ptr()),enc.reg(idx()),_off,_scale); + } + + // General form: "add 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/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddX86.java new file mode 100644 index 00000000..84543e8f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class AddX86 extends RegX86 { + AddX86( Node add ) { super(add); } + @Override public String op() { return "add"; } + @Override public String glabel() { return "+"; } + @Override int opcode() { return 0x03; } + @Override public boolean commutes() { return true; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndIX86.java new file mode 100644 index 00000000..3c1b567c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndIX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class AndIX86 extends ImmX86 { + AndIX86( Node add, int imm ) { super(add,imm); } + @Override public String op() { return "andi"; } + @Override public String glabel() { return "&"; } + @Override int opcode() { return 0x81; } + @Override int mod() { return 4; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndX86.java new file mode 100644 index 00000000..dedbce45 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AndX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class AndX86 extends RegX86 { + AndX86( Node add ) { super(add); } + @Override public String op() { return "and"; } + @Override public String glabel() { return "&"; } + @Override int opcode() { return 0x23; } + @Override public boolean commutes() { return true; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallRX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallRX86.java new file mode 100644 index 00000000..f97163eb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallRX86.java @@ -0,0 +1,30 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.CallNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; + +public class CallRX86 extends CallNode implements MachNode { + CallRX86( CallNode call ) { super(call); } + @Override public String op() { return "callr"; } + @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(tfp(),i,fun()._maxArgSlot); // Normal argument + } + @Override public RegMask outregmap() { return null; } + @Override public void encoding( Encoding enc ) { + X86.callRegister(enc,enc.reg(fptr())); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(fptr())).p(" "); + for( int i=0; i<nargs(); i++ ) + sb.p(code.reg(arg(i))).p(" "); + sb.unchar(2); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallX86.java new file mode 100644 index 00000000..6bafbf37 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallX86.java @@ -0,0 +1,55 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; + +public class CallX86 extends CallNode implements MachNode, RIPRelSize { + final TypeFunPtr _tfp; + final String _name; + final boolean _extern; + CallX86( CallNode call, TypeFunPtr tfp ) { + super(call); + _inputs.pop(); // Pop constant target + assert tfp.isConstant(); + _tfp = tfp; + _name = CodeGen.CODE.funcName(tfp.fidx()); + _extern = CodeGen.CODE.externFunc(tfp.fidx()) != null; + } + @Override public String op() { return "call"; } + @Override public String label() { return op(); } + @Override public String name() { return _name; } + @Override public TypeFunPtr tfp() { return _tfp; } + @Override public RegMask regmap(int i) { + return _extern + ? x86_64_v2.callInMask(_tfp,i,fun()._maxArgSlot,CodeGen.CODE._cCallingConv) + : x86_64_v2.callInMask(_tfp,i,fun()._maxArgSlot); + } + @Override public RegMask outregmap() { return null; } + @Override public int nargs() { return nIns()-2; } // Minus control, memory, fptr + @Override public boolean external() { return _extern; } + + @Override public void encoding( Encoding enc ) { + FunNode fun = CodeGen.CODE.link(_tfp); + if( fun==null ) enc.external(this,_name); + else enc.relo(this); + X86.call(enc); + } + + // Delta is from opcode start, but X86 measures from the end of the 5-byte encoding + @Override public byte encSize(int delta) { return 5; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + enc.patch4(opStart+1, delta-5); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(_name).p(" "); + for( int i=0; i<nargs(); i++ ) + sb.p(code.reg(in(i+2))).p(", "); + sb.unchar(2); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpFX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpFX86.java new file mode 100644 index 00000000..5ef7a476 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpFX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class CmpFX86 extends MachConcreteNode implements MachNode { + CmpFX86( Node cmp ) { super(cmp); } + @Override public String op() { return "cmpf"; } + @Override public RegMask regmap(int i) { return x86_64_v2.XMASK; } + @Override public RegMask outregmap() { return x86_64_v2.FLAGS_MASK; } + + @Override public void encoding( Encoding enc ) { + X86.sse(enc,0x66,0x0F2E,false,enc.reg(in(1))-x86_64_v2.XMM_OFFSET,enc.reg(in(2))-x86_64_v2.XMM_OFFSET); + } + + // General form: "cmp src1,src2" + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpIX86.java new file mode 100644 index 00000000..151f8ca8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpIX86.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// Compare immediate. Sets flags. +public class CmpIX86 extends MachConcreteNode implements MachNode { + final int _imm; + CmpIX86( Node cmp, int imm, boolean swap ) { + super(cmp); + _inputs.del(swap ? 1 : 2); + _imm = imm; + } + CmpIX86( CmpIX86 cmp ) { + super((Node)null); + addDef(null); + addDef(cmp.in(1)); + _imm = cmp._imm; + } + + @Override public String op() { return _imm==0 ? "test" : "cmp"; } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.FLAGS_MASK; } + @Override public boolean isClone() { return true; } + @Override public Node copy() { return new CmpIX86(this); } + + @Override public final void encoding( Encoding enc ) { + X86.imm(enc,0x81,7,enc.reg(in(1)),_imm); + } + @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); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpMemX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpMemX86.java new file mode 100644 index 00000000..4014045b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpMemX86.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.util.SB; + +public class CmpMemX86 extends MemOpX86 { + final boolean _swap; // Op switched LHS, RHS + CmpMemX86( BoolNode bool, LoadNode ld, Node base, Node idx, int off, int scale, int imm, Node val, boolean swap ) { + super(bool,ld, base, idx, off, scale, imm, val ); + _swap = swap; + assert !_swap || val() != null || bool.op()=="==" || bool.op()=="!="; // Cannot swap with immediate + } + @Override public String op() { return ((val()==null && _imm==0) ? "test" : "cmp") + _sz; } + @Override public RegMask outregmap() { return x86_64_v2.FLAGS_MASK; } + @Override public void encoding( Encoding enc ) { + short ptr=enc.reg(ptr()), idx=enc.reg(idx()), src=enc.reg(val()); + if( src==-1 ) { + assert !_swap; + X86.cmpImm(enc,_con.log_size(),_imm,ptr,idx,_off,_scale); + } else encVal(enc,_con,ptr,idx,src,_off,_scale,_swap); + } + + static void encVal(Encoding enc, Type decl, short ptr, short idx, short src, int off, int scale, boolean _swap) { + assert !(decl instanceof TypeFloat); // Selected separately as CmpFX86. + X86.cmpMem(enc,decl.log_size(),src,ptr,idx,off,scale,_swap); + } + + // 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( _swap ) asm_address(code,sb).p(","); + if( val()==null ) { + if( _imm!=0 ) sb.p("#").p(_imm); + } else { + sb.p(code.reg(val())); + } + if( !_swap ) asm_address(code,sb.p(", ")); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpX86.java new file mode 100644 index 00000000..7faea17b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpX86.java @@ -0,0 +1,28 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class CmpX86 extends MachConcreteNode implements MachNode { + CmpX86( Node add ) { super(add); } + @Override public String op() { return "cmp"; } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.FLAGS_MASK; } + // This one is marginal: cloning a Cmp[reg,reg] means stretching the + // lifetimes of 2 normal registers in exchange for shortening a flags + // register lifetime. Spilling flags is (probably) relatively expensive + // compared to some normal registers - and those normal registers might not + // be spilling! + @Override public boolean isClone() { return true; } + + @Override public void encoding( Encoding enc ) { + X86.reg(enc,0x3B,enc.reg(in(1)),enc.reg(in(2))); + } + @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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivFX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivFX86.java new file mode 100644 index 00000000..7bbb5102 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivFX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class DivFX86 extends MachConcreteNode implements MachNode { + DivFX86( Node divf) { super(divf); } + @Override public String op() { return "divf"; } + @Override public RegMask regmap(int i) { return x86_64_v2.XMASK; } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + @Override public int twoAddress() { return 1; } + + @Override public void encoding( Encoding enc ) { + X86.sse(enc,0xF2,0x0F5E,false,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(in(2))-x86_64_v2.XMM_OFFSET); + } + + + // General form: "divf dst /= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" /= ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivX86.java new file mode 100644 index 00000000..9f375a80 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/DivX86.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class DivX86 extends MachConcreteNode implements MachNode { + DivX86( Node div ) { super(div); } + @Override public String op() { return "div"; } + @Override public RegMask regmap(int i) { + return (i==1) ? x86_64_v2.RAX_MASK : x86_64_v2.DIV_MASK; + } + @Override public RegMask outregmap() { return x86_64_v2.RAX_MASK; } + @Override public RegMask killmap() { return x86_64_v2.RDX_MASK; } + + @Override public void encoding( Encoding enc ) { + X86.divide(enc,enc.reg(in(2))); + } + // 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 comment() { return "kill rdx"; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java new file mode 100644 index 00000000..1a6ae123 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +// Convert a narrow C return register to Simple's full-width integer value. +public class ExtendX86 extends MachConcreteNode { + final int _bits; + final boolean _signed; + ExtendX86(ProjNode prj, TypeInteger ret) { + super(prj); + _inputs.set(0,null); + _inputs.add(new ProjX86(prj)); + _bits = 8 << ret.log_size(); + _signed = ret._min < 0; + } + @Override public String op() { return (_signed ? "sext" : "zext")+_bits; } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public void encoding(Encoding enc) { + X86.extend(enc,_bits,_signed,enc.reg(this),enc.reg(in(1))); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/F8ToIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/F8ToIX86.java new file mode 100644 index 00000000..a79a1017 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/F8ToIX86.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class F8ToIX86 extends MachConcreteNode implements MachNode { + F8ToIX86(Node f8toi ) { super(f8toi); } + @Override public String op() { return "cvti"; } + @Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.XMASK; } + @Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + @Override public void encoding( Encoding enc ) { + X86.floatToInteger(enc,enc.reg(this),enc.reg(in(1))-x86_64_v2.XMM_OFFSET); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p("(int)").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FltX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FltX86.java new file mode 100644 index 00000000..59f06341 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FltX86.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class FltX86 extends ConstantNode implements MachNode, RIPRelSize{ + private byte _encSize; + final String _ext; + FltX86( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { return "ld8"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + @Override public boolean isClone() { return true; } + @Override public Node copy() { return new FltX86(this,_ext); } + + @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); + _encSize=X86.rip(enc,0xF2,0x0F10,false,enc.reg(this)-x86_64_v2.XMM_OFFSET); + enc.largeConstant(this,_con,_encSize-4,2/*ELF PC32*/); + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { + // TODO: 1-byte RIP offset? + return (byte)_encSize; + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + enc.patch4(opStart+opLen-4,delta-opLen); + } + + @Override public void asm(CodeGen code, SB sb) { + _con.print(sb.p(code.reg(this)).p(" = #")); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FunX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FunX86.java new file mode 100644 index 00000000..0e44aee7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FunX86.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.util.SB; + +public class FunX86 extends FunNode implements MachNode { + FunX86( FunNode fun ) { super(fun); } + @Override public void postSelect(CodeGen code) { + super.postSelect(code); + _maxArgSlot = code._mach.maxArgSlot(sig(),code._callingConv); + // One more for RPC pre-pushed; X86 special + _maxArgSlot++; + } + + @Override public void computeFrameAdjust(CodeGen code, int maxReg) { + super.computeFrameAdjust(code,maxReg); + // Alignment, but off by RPC + if( _hasCalls ) // If non-leaf, pad to 16b + _frameAdjust = ((_frameAdjust+8+8) & -16)-8; + } + + @Override public void encoding( Encoding enc ) { + int sz = _frameAdjust; + if( sz == 0 ) return; // Skip if no frame adjust + X86.imm(enc,0x81,5,x86_64_v2.RSP,sz); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/I2f8X86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/I2f8X86.java new file mode 100644 index 00000000..ee357901 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/I2f8X86.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class I2f8X86 extends MachConcreteNode implements MachNode { + I2f8X86(Node i2f8 ) { super(i2f8); } + @Override public String op() { return "cvtf"; } + @Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.WMASK; } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + + @Override public void encoding( Encoding enc ) { + X86.sse(enc,0xF2,0x0F2A,true,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(in(1))); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = ").p("(flt)").p(code.reg(in(1))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ImmX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ImmX86.java new file mode 100644 index 00000000..5a4ff4b2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ImmX86.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; + +public abstract class ImmX86 extends MachConcreteNode implements MachNode { + final int _imm; + ImmX86( Node add, int imm ) { + super(add); + _inputs.pop(); // Pop constant input off + _imm = imm; + } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public int twoAddress() { return 1; } + @Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; } + + abstract int opcode(); + abstract int mod(); + + @Override public void encoding( Encoding enc ) { + X86.imm(enc,opcode(),mod(),enc.reg(this),_imm); + } + + // General form: "addi dst += #imm" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" ").p(glabel()).p("= #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/IntX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/IntX86.java new file mode 100644 index 00000000..2ffaa3d1 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/IntX86.java @@ -0,0 +1,43 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Integer constants +public class IntX86 extends ConstantNode implements MachNode { + final String _ext; + IntX86( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { + return _con == Type.NIL || _con == TypeInteger.ZERO ? "xor" : "ldi"; + } + @Override public boolean isClone() { return true; } + @Override public Node copy() { return new IntX86(this,_ext); } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + // Zero-set uses XOR kills flags + @Override public RegMask killmap() { + return _con == Type.NIL || _con == TypeInteger.ZERO ? x86_64_v2.FLAGS_MASK : null; + } + + @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); + X86.constant(enc,enc.reg(this),_con==Type.NIL ? 0 : ((TypeInteger)_con).value()); + } + + // 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(" = #")); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java new file mode 100644 index 00000000..10cee059 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/JmpX86.java @@ -0,0 +1,65 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Jump on flags, uses flags +public class JmpX86 extends IfNode implements MachNode, RIPRelSize { + String _bop; + JmpX86( IfNode iff, String bop ) { + super(iff); + _bop = bop; + } + @Override public String op() { return "j"+_bop; } + @Override public String label() { return op(); } + @Override public void postSelect(CodeGen code) { + Node set = in(1); + if( set==null ) return; // Never-node cutout + Node cmp = set.in(1); + // Bypass an expected Set and just reference the cmp directly + if( set instanceof SetX86 setx && (cmp instanceof CmpX86 || cmp instanceof CmpIX86 || cmp instanceof CmpMemX86 || cmp instanceof CmpFX86) ) { + _inputs.set( 1, cmp ); + _bop = setx._bop; + } else + throw Utils.TODO(); + } + @Override public RegMask regmap(int i) { assert i==1; return x86_64_v2.FLAGS_MASK; } + @Override public RegMask outregmap() { return null; } + @Override public void negate() { _bop = negate(_bop); } + + @Override public void encoding( Encoding enc ) { + if( in(1)==null && _bop=="!=" ) return; + X86.branch(enc,in(1)==null ? -1 : x86_64_v2.jumpop(_bop)&15); + enc.jump(this,cproj(0)); + } + + // Delta is from opcode start, but X86 measures from the end of the 2-byte encoding + @Override public byte encSize(int delta) { + if( in(1)==null && _bop=="!=" ) return 0; + return X86.branchSize(delta,in(1)!=null); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + assert !(in(1)==null && _bop=="!="); + X86.patchBranch(enc.bits(),opStart,opLen,delta,in(1)==null ? -1 : x86_64_v2.jumpop(_bop)&15); + } + + @Override public void asm(CodeGen code, SB sb) { + if( in(1)!=null ) { // Never-node + String src = code.reg(in(1)); + if( src!="flags" ) sb.p(src).p(" "); + } else if( _bop=="!=" ) { + sb.p("never"); + return; + } + CFGNode prj = cproj(0).uctrlSkipEmpty(); // 0 is True is jump target + if( !prj.blockHead() ) prj = prj.cfg0(); + sb.p(label(prj)); + } + + @Override public String comment() { return "L"+cproj(1)._nid; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LeaX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LeaX86.java new file mode 100644 index 00000000..4b00e4dd --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LeaX86.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class LeaX86 extends MachConcreteNode implements MachNode { + final int _scale; + final int _offset; + LeaX86( Node add, Node base, Node idx, int scale, int offset ) { + super(add); + assert scale==0 || scale==1 || scale==2 || scale==3; + _inputs.pop(); + _inputs.pop(); + _inputs.push(base); + _inputs.push(idx); + _scale = scale; + _offset = offset; + } + + @Override public String op() { return "lea"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + + @Override public void encoding( Encoding enc ) { + X86.lea(enc,enc.reg(this),enc.reg(in(1)),enc.reg(in(2)),_offset,_scale); + } + + // 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); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java new file mode 100644 index 00000000..5c472dbe --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.LoadNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +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; } + @Override public RegMask outregmap() { return _con instanceof TypeFloat ? x86_64_v2.XMASK : x86_64_v2.WMASK; } + @Override public void encoding( Encoding enc ) { + // REX.W + 8B /r MOV r64, r/m64 + // Zero extension for u8, u16 and u32 but sign extension i8, i16, i32 + // Use movsx and movzx + short dst = enc.reg(this ); + short ptr = enc.reg(ptr()); + short idx = enc.reg(idx()); + enc(enc, _con, dst, ptr, idx, _off, _scale); + } + + static void enc( Encoding enc, Type decl, short dst, short ptr, short idx, int off, int scale ) { + // The chapter chooses the register bank and extension from its own type facts. + boolean xmm=dst>=x86_64_v2.XMM_OFFSET; + if( xmm && decl.log_size()==3 ) decl=TypeFloat.F64; + else if( !xmm && decl instanceof TypeFloat ) + decl=decl==TypeFloat.F32 ? TypeInteger.U32 : TypeInteger.BOT; + boolean signed=decl.isa(TypeInteger.I8) || decl==TypeInteger.I16 || decl==TypeInteger.I32; + X86.load(enc,decl.log_size(),signed,xmm, + xmm ? dst-x86_64_v2.XMM_OFFSET : dst,ptr,idx,off,scale); + } + + + // 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/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemAddX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemAddX86.java new file mode 100644 index 00000000..2e18a058 --- /dev/null +++ b/chapter-modules/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.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +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 ); + } + @Override public String op() { + return (_imm == 1 ? "inc" : (_imm == -1 ? "dec" : "add")) + _sz; + } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return null; } + @Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; } + @Override public void encoding( Encoding enc ) { + X86.addMem(enc,enc.reg(val()),_imm,enc.reg(ptr()),enc.reg(idx()),_off,_scale); + } + // 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())); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java new file mode 100644 index 00000000..fcca7f8f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java @@ -0,0 +1,89 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import java.lang.StringBuilder; + +// 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 an 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; // Handy print name + 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 e.g. 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; + int size = mop instanceof StoreNode st ? st.storeSize() : 1<<_con.log_size(); + _sz = (char)('0'+size); + } + + // 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); } // Only for stores, including op-to-memory + + @Override protected ExprPrinter<Node> _printMach(ExprPrinter<Node> p) { + return p.p(".").p(_name); + } + + @Override public String label() { return op(); } + @Override public Type compute() { throw new AssertionError("Do not call compute on "+getClass().getSimpleName()); } + @Override public Node idealize() { throw new AssertionError("Do not call idealize on "+getClass().getSimpleName()); } + + // Register mask allowed on input i. + @Override public RegMask regmap(int i) { + if( i==1 ) return null; // Memory + if( i==2 ) return x86_64_v2.RMASK; // base in GPR + if( i==3 ) return x86_64_v2.RMASK; // index in GPR + if( i==4 ) return _sz >= '4' + ? x86_64_v2.MEM_MASK // value in GPR or XMM + : x86_64_v2.RMASK; // Bytes and shorts in GPR only + return null; // Anti-dependence + } + + // "[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((1<<_scale)); + } + if( _off != 0 ) + sb.p("+").p(_off); + return sb.p("]"); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulFX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulFX86.java new file mode 100644 index 00000000..74616db7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulFX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class MulFX86 extends MachConcreteNode implements MachNode { + MulFX86( Node mulf) { super(mulf); } + @Override public String op() { return "mulf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + @Override public int twoAddress() { return 1; } + @Override public boolean commutes() { return true; } + + @Override public void encoding( Encoding enc ) { + X86.sse(enc,0xF2,0x0F59,false,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(in(2))-x86_64_v2.XMM_OFFSET); + } + + // General form: "mulf dst *= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" *= ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulIX86.java new file mode 100644 index 00000000..5dcb253d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulIX86.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class MulIX86 extends MachConcreteNode implements MachNode { + final int _imm; + MulIX86( Node add, int imm ) { + super(add); + _inputs.pop(); // Pop the constant input + _imm = imm; // Record the constant + } + @Override public String op() { return "muli"; } + @Override public String glabel() { return "*"; } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + // Rare 3-address form + @Override public final void encoding( Encoding enc ) { + X86.imul(enc,enc.reg(this),enc.reg(in(1)),_imm); + } + // 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(" * #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulX86.java new file mode 100644 index 00000000..6f3fcdeb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MulX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class MulX86 extends MachConcreteNode implements MachNode { + MulX86( Node mul ) { super(mul); } + @Override public String op() { return "mul"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public int twoAddress() { return 1; } + @Override public boolean commutes() { return true; } + + @Override public void encoding( Encoding enc ) { + X86.reg(enc,0x0FAF,enc.reg(this),enc.reg(in(2))); + } + + // 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))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java new file mode 100644 index 00000000..86610f6d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegX86.java @@ -0,0 +1,20 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class NegX86 extends MachConcreteNode implements MachNode { + NegX86(MinusNode not) { super(not); } + @Override public String op() { return "neg"; } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.RMASK; } + @Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; } + @Override public int twoAddress() { return 1; } + + @Override public void encoding( Encoding enc ) { + X86.unary(enc,0xF7,3,enc.reg(this)); + } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(this)); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java new file mode 100644 index 00000000..d6a06c2f --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java @@ -0,0 +1,24 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; + +public class NewX86 extends NewNode implements MachNode { + // A pre-zeroed chunk of memory. + NewX86( NewNode nnn ) { super(nnn); } + @Override public void encoding( Encoding enc ) { + enc.external(this,"calloc"); + X86.constant(enc,_arg2Reg,1); + X86.call(enc); + } + // General form: "alloc #bytes PC" + @Override public void asm(CodeGen code, SB sb) { + sb.p("\n"); // Multi-op trigger + sb.p("ldi ").p(code._mach.regs()[_arg2Reg]).p(" = #1\n"); + sb.p("call #calloc"); + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java new file mode 100644 index 00000000..1e4e7c26 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NotX86.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class NotX86 extends MachConcreteNode implements MachNode { + NotX86(NotNode not) { super(not); } + @Override public String op() { return "not"; } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.RMASK; } + @Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; } + + @Override public void encoding( Encoding enc ) { + assert !(in(1) instanceof NotNode); // Cleared out by peeps + assert !(in(1) instanceof BoolNode); + X86.not(enc,enc.reg(this),enc.reg(in(1))); + } + @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(this)); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrIX86.java new file mode 100644 index 00000000..15f4824d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrIX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class OrIX86 extends ImmX86 { + OrIX86( Node add, int imm ) { super(add,imm); } + @Override public String op() { return "ori"; } + @Override public String glabel() { return "|"; } + @Override int opcode() { return 0x81; } + @Override int mod() { return 1; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrX86.java new file mode 100644 index 00000000..bc8f0ea8 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/OrX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class OrX86 extends RegX86 { + OrX86( Node add ) { super(add); } + @Override public String op() { return "or"; } + @Override public String glabel() { return "|"; } + @Override int opcode() { return 0x0B; } + @Override public boolean commutes() { return true; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ParmX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ParmX86.java new file mode 100644 index 00000000..5cfdd804 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ParmX86.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ParmNode; + +public class ParmX86 extends ParmNode implements MachNode { + final RegMask _rmask; + ParmX86( ParmNode parm ) { + super(parm); + _rmask = x86_64_v2.callInMask(fun().sig(),_idx,1/*RPC*/); + } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return _rmask; } + @Override public void encoding( Encoding enc ) { } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ProjX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ProjX86.java new file mode 100644 index 00000000..046c7b4c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ProjX86.java @@ -0,0 +1,15 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.ProjNode; + +public class ProjX86 extends ProjNode implements MachNode { + ProjX86( ProjNode p ) { super(p); } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { + return ((MachNode)in(0)).outregmap(_idx); + } + @Override public void encoding( Encoding enc ) {} +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RegX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RegX86.java new file mode 100644 index 00000000..06755143 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RegX86.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public abstract class RegX86 extends MachConcreteNode { + RegX86( Node add ) { super(add); } + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public RegMask killmap() { return x86_64_v2.FLAGS_MASK; } + @Override public int twoAddress() { return 1; } + abstract int opcode(); + @Override public void encoding( Encoding enc ) { + X86.reg(enc,opcode(),enc.reg(in(1)),enc.reg(in(2))); + } + // General form: "add dst += src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" ").p(glabel()).p("= ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RetX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RetX86.java new file mode 100644 index 00000000..e4467bb4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/RetX86.java @@ -0,0 +1,15 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.*; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; + +public class RetX86 extends ReturnNode implements MachNode { + RetX86( ReturnNode ret, FunNode fun ) { super(ret, fun); fun.setRet(this); } + @Override public void encoding( Encoding enc ) { + int sz = fun()._frameAdjust; + if( sz!=0 ) X86.imm(enc,0x81,0,x86_64_v2.RSP,sz); + X86.ret(enc); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarIX86.java new file mode 100644 index 00000000..cf6275dc --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarIX86.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.RegMask; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +// Arithmetic Right Shift +public class SarIX86 extends MachConcreteNode implements MachNode { + final int _imm; + SarIX86( Node sari, int imm ) { super(sari); assert x86_64_v2.imm8(imm); _inputs.pop(); _imm = imm; } + @Override public String op() { return "sari"; } + @Override public String glabel() { return ">>"; } + int opcode() { return 0xC1; } + int mod() { return 7; } + + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public int twoAddress() { return 1; } + + @Override public void encoding(Encoding enc) { + X86.shift(enc,mod(),enc.reg(this),_imm); + } + + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" ").p(glabel()).p("= #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarX86.java new file mode 100644 index 00000000..92493712 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SarX86.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class SarX86 extends RegX86 { + SarX86( Node add ) { super(add); } + @Override public String op() { return "sar"; } + @Override public String glabel() { return ">>"; } + @Override public RegMask regmap(int i) { + if (i == 1) return x86_64_v2.WMASK; + if (i == 2) return x86_64_v2.RCX_MASK; + throw Utils.TODO(); + } + @Override int opcode() { return 0xD3; } + @Override public void encoding( Encoding enc ) { + X86.unary(enc,opcode(),7,enc.reg(this)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java new file mode 100644 index 00000000..864c1b55 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SetX86.java @@ -0,0 +1,44 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// 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 <,<=,== + final boolean _unsigned; + // Constructor expects input is an X86 and not an Ideal node. + SetX86( Node cmp, String bop, boolean unsigned ) { + super(cmp); + _inputs.setLen(1); // Pop the cmp inputs + // Replace with the matched cmp + _inputs.push(cmp); + _bop = bop; + _unsigned = unsigned; + } + @Override public String op() { return "set"+_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; } + + @Override public void encoding( Encoding enc ) { + int op=switch (_bop) { + case "==" -> 0x94; // SETE + case "<" -> _unsigned ? 0x92 : 0x9C; // SETB /SETL + case "<=" -> _unsigned ? 0x96 : 0x9E; // SETBE/SETLE + case ">=" -> _unsigned ? 0x93 : 0x9D; // SETAE/SETGE + case ">" -> _unsigned ? 0x97 : 0x9F; // SETA /SETG + default -> throw Utils.TODO(); + }; + X86.set(enc,op&15,enc.reg(this)); + } + + @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); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlIX86.java new file mode 100644 index 00000000..1e62243e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlIX86.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +public class ShlIX86 extends MachConcreteNode implements MachNode { + final int _imm; + ShlIX86( Node shli, int imm ) { + super(shli); + assert x86_64_v2.imm8(imm); + _inputs.pop(); // Pop constant input off + _imm = imm; + } + public int opcode() { return 0xC1; } + public int mod() { return 4; } + + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public int twoAddress() { return 1; } + + @Override public String op() { return "shli"; } + @Override public String glabel() { return "<<"; } + @Override public void encoding(Encoding enc) { + X86.shift(enc,mod(),enc.reg(this),_imm); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" ").p(glabel()).p("= #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlX86.java new file mode 100644 index 00000000..768d97e7 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShlX86.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class ShlX86 extends RegX86 { + ShlX86( Node add ) { super(add); } + @Override public String op() { return "shl"; } + @Override public String glabel() { return "<<"; } + @Override public RegMask regmap(int i) { + if (i == 1) return x86_64_v2.WMASK; + if (i == 2) return x86_64_v2.RCX_MASK; + throw Utils.TODO(); + } + @Override int opcode() { return 0xD3; } + @Override public final void encoding( Encoding enc ) { + X86.unary(enc,opcode(),4,enc.reg(this)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrIX86.java new file mode 100644 index 00000000..8a0d1b58 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrIX86.java @@ -0,0 +1,27 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.SB; + +public class ShrIX86 extends MachConcreteNode implements MachNode { + final int _imm; + ShrIX86( Node shri, int imm ) { super(shri); assert x86_64_v2.imm8(imm); _inputs.pop(); _imm = imm; } + @Override public String op() { return "shri"; } + @Override public String glabel() { return ">>>"; } + int opcode() { return 0xC1; } + int mod() { return 5; } + + @Override public RegMask regmap(int i) { return x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public int twoAddress() { return 1; } + + @Override public void encoding(Encoding enc) { + X86.shift(enc,mod(),enc.reg(this),_imm); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" ").p(glabel()).p("= #").p(_imm); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrX86.java new file mode 100644 index 00000000..1ba20fee --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ShrX86.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.util.Utils; + +public class ShrX86 extends RegX86 { + ShrX86( Node add ) { super(add); } + @Override public String op() { return "shr"; } + @Override public String glabel() { return ">>>"; } + @Override public RegMask regmap(int i) { + if (i == 1) return x86_64_v2.WMASK; + if (i == 2) return x86_64_v2.RCX_MASK; + throw Utils.TODO(); + } + @Override int opcode() { return 0xD3; } + @Override public final void encoding( Encoding enc ) { + X86.unary(enc,opcode(),5,enc.reg(this)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitFlagsX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitFlagsX86.java new file mode 100644 index 00000000..cb7694f9 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitFlagsX86.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; + +// Flag snapshots can use GPRs or stack slots, but never XMM registers. +public class SplitFlagsX86 extends SplitX86 { + private static final RegMask MASK = new RegMask(x86_64_v2.WR_BITS | (1L<<x86_64_v2.FLAGS) | x86_64_v2.SPILLS,-1L); + SplitFlagsX86(LRG lrg,String kind,byte round) { super(lrg,kind,round); } + @Override public RegMask regmap(int i) { return MASK; } + @Override public RegMask outregmap() { return MASK; } + + @Override public void encoding(Encoding enc) { + short dst=enc.reg(this), src=enc.reg(in(1)); + if( dst==src ) return; + if( dst==x86_64_v2.FLAGS || src==x86_64_v2.FLAGS ) { + boolean toFlags = dst==x86_64_v2.FLAGS; + int reg = toFlags ? src : dst; + if( reg>=x86_64_v2.MAX_REG ) + X86.flagsStack(enc,fun(enc).computeStackOffset(enc._code,reg),toFlags); + else + X86.flags(enc,reg,toFlags); + } else super.encoding(enc); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitX86.java new file mode 100644 index 00000000..46f2cb16 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitX86.java @@ -0,0 +1,45 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.SplitNode; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +public class SplitX86 extends SplitNode { + SplitX86( LRG lrg, String kind, byte round ) { super(lrg,kind,round, new Node[2]); } + @Override public RegMask regmap(int i) { return x86_64_v2.SPLIT_MASK; } + @Override public RegMask outregmap() { return x86_64_v2.SPLIT_MASK; } + + // Moves among GPRs, XMM registers, and stack slots. + @Override public void encoding( Encoding enc ) { + short dst=enc.reg(this), src=enc.reg(in(1)); + assert dst!=x86_64_v2.FLAGS && src!=x86_64_v2.FLAGS; + boolean dstX=dst>=x86_64_v2.XMM_OFFSET, srcX=src>=x86_64_v2.XMM_OFFSET; + // Stack spills + if( dst >= x86_64_v2.MAX_REG ) { + int off = fun(enc).computeStackOffset(enc._code,dst); + if( src >= x86_64_v2.MAX_REG ) { + // Rare stack-stack move. push [RSP+soff]; pop [RSP+doff] + int soff = fun(enc).computeStackOffset(enc._code,src); + X86.stackCopy(enc,soff,off); + return; + } + StoreX86.encVal(enc, srcX ? TypeFloat.F64 : TypeInteger.BOT, (short)x86_64_v2.RSP, (short)-1/*index*/, src, off, 0); + return; + } + if( src >= x86_64_v2.MAX_REG ) { + int off = fun(enc).computeStackOffset(enc._code,src); + LoadX86.enc(enc, dstX ? TypeFloat.F64 : TypeInteger.BOT, dst, (short)x86_64_v2.RSP, (short)-1, off, 0); + return; + } + + X86.move(enc,dstX,srcX,dstX ? dst-x86_64_v2.XMM_OFFSET : dst, + srcX ? src-x86_64_v2.XMM_OFFSET : src); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java new file mode 100644 index 00000000..9ffa25fc --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java @@ -0,0 +1,48 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.print.ExprPrinter; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StoreNode; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.util.SB; + +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 protected ExprPrinter<Node> _printMach(ExprPrinter<Node> p) { + Node val = val(); + p.p(".").p(_name).p("="); + if( val==null ) p.p(_imm); + else p.n(val); + return p.p(";"); + } + // Register mask allowed as a result. 0 for no register. + @Override public RegMask outregmap() { return null; } + @Override public void encoding( Encoding enc ) { + short ptr=enc.reg(ptr()), idx=enc.reg(idx()), src=enc.reg(val()); + if( src==-1 ) X86.storeImm(enc,Integer.numberOfTrailingZeros(_sz-'0'),_imm,ptr,idx,_off,_scale); + else encVal(enc,_con,ptr,idx,src,_off,_scale); + } + + // Non-immediate encoding + static void encVal( Encoding enc, Type decl, short ptr, short idx, short src, int off, int scale ) { + boolean xmm=src>=x86_64_v2.XMM_OFFSET; + X86.store(enc,decl.log_size(),xmm, + xmm ? src-x86_64_v2.XMM_OFFSET : src,ptr,idx,off,scale); + } + + + // 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/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubFX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubFX86.java new file mode 100644 index 00000000..29f0033a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubFX86.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +public class SubFX86 extends MachConcreteNode implements MachNode { + SubFX86( Node subf) { super(subf); } + @Override public String op() { return "subf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + @Override public int twoAddress() { return 1; } + + @Override public void encoding( Encoding enc ) { + X86.sse(enc,0xF2,0x0F5C,false,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(in(2))-x86_64_v2.XMM_OFFSET); + } + + // General form: "subf dst -= src" + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" -= ").p(code.reg(in(2))); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubX86.java new file mode 100644 index 00000000..8da34bf5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SubX86.java @@ -0,0 +1,10 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class SubX86 extends RegX86 { + SubX86( Node add ) { super(add); } + @Override public String op() { return "sub"; } + @Override public String glabel() { return "-"; } + @Override int opcode() { return 0x2B; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java new file mode 100644 index 00000000..e3563713 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java @@ -0,0 +1,51 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// Function constants +public class TFPX86 extends FunPtrNode implements MachNode, RIPRelSize { + private byte _opLen; + final String _ext; // External name + // Pointer to a Simple function + TFPX86( FunPtrNode fptr ) { this((TypeFunPtr)fptr._type,null); } + // Spill clone: copy inputs without registering their reverse edges; + // insertBefore will place and register the clone in its use block. + private TFPX86( TFPX86 fptr ) { super(fptr); _ext = fptr._ext; } + // Pointer to an Extern "C" function + TFPX86( TypeFunPtr fptr, String ext ) { super(fptr,CodeGen.CODE._start,null); _type = fptr; _ext = ext; } + @Override public String op() { return "ldx"; } + @Override public String label() { return op(); } + @Override public boolean isClone() { return true; } + @Override public TFPX86 copy() { return new TFPX86(this); } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public void encoding( Encoding enc ) { + if( _ext==null ) enc.relo(this); + else enc.external(this,_ext); + _opLen=X86.rip(enc,0,0x8D,true,enc.reg(this)); + } + + @Override public byte encSize(int delta) { return _opLen; } + + // Delta is from opcode start, X86 expects it from opcode end + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + enc.patch4(opStart + opLen - 4, delta - opLen); + } + + // 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(this)); + if( _ext == null ) _type.print(sb.p(" #")); + else sb.p(_ext); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java new file mode 100644 index 00000000..682597f4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeMemPtr; +import com.seaofnodes.simple.util.SB; + +public class TMPX86 extends ConstantNode implements MachNode, RIPRelSize{ + private byte _opLen; + final String _ext; + TMPX86( ConstantNode con, String ext ) { super(con); _ext = ext; } + @Override public String op() { return "ldp"; } + @Override public RegMask regmap(int i) { return null; } + @Override public RegMask outregmap() { return x86_64_v2.WMASK; } + @Override public boolean isClone() { return true; } + @Override public Node copy() { return new TMPX86(this,_ext); } + + @Override public void encoding( Encoding enc ) { + short dst = enc.reg(this); + // 0 or 1 for REX depending on the dst. + _opLen = X86.rip(enc,0,0x8D,true,dst); + if( _ext!=null ) enc.externalData(this,_ext); + else enc.largeConstant(this,((TypeMemPtr)_con)._obj, _opLen-4, 2/*ELF encoding PC32*/); + } + + // Delta is from opcode start + @Override public byte encSize(int delta) { return _opLen; } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + enc.patch4(opStart + opLen - 4, delta - opLen); + } + + @Override public void asm(CodeGen code, SB sb) { + _con.print(sb.p(code.reg(this)).p(" = #")); + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/UJmpX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/UJmpX86.java new file mode 100644 index 00000000..591a51c3 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/UJmpX86.java @@ -0,0 +1,41 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; + +// unconditional jump +public class UJmpX86 extends CFGNode implements MachNode, RIPRelSize { + UJmpX86() { } + @Override public String op() { return "jmp"; } + @Override public String label() { return op(); } + @Override protected String format() { return "jmp "; } + @Override public RegMask regmap(int i) {return null; } + @Override public RegMask outregmap() { return null; } + @Override public Type compute() { throw Utils.TODO(); } + @Override public Node idealize() { throw Utils.TODO(); } + @Override public void encoding( Encoding enc ) { + enc.jump(this,uctrl()); + X86.branch(enc,-1); + } + + // Delta is from opcode start, but X86 measures from the end of the 2-byte encoding + @Override public byte encSize(int delta) { + return X86.branchSize(delta,false); + } + + // Delta is from opcode start + @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { + X86.patchBranch(enc.bits(),opStart,opLen,delta,-1); + } + + @Override public void asm(CodeGen code, SB sb) { + CFGNode target = uctrl(); + //assert target.nOuts() > 1; // Should optimize jmp to empty targets + sb.p(label(target)); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorIX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorIX86.java new file mode 100644 index 00000000..2d989a50 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorIX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class XorIX86 extends ImmX86 { + XorIX86( Node add, int imm ) { super(add,imm); } + @Override public String op() { return "xori"; } + @Override public String glabel() { return "^"; } + @Override int opcode() { return 0x81; } + @Override int mod() { return 6; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorX86.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorX86.java new file mode 100644 index 00000000..86703903 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/XorX86.java @@ -0,0 +1,11 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.node.Node; + +public class XorX86 extends RegX86 { + XorX86( Node add ) { super(add); } + @Override public String op() { return "xor"; } + @Override public String glabel() { return "^"; } + @Override int opcode() { return 0x33; } + @Override public boolean commutes() { return true; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java new file mode 100644 index 00000000..3dc93bf6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java @@ -0,0 +1,596 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + +public class x86_64_v2 extends Machine { + public x86_64_v2( CodeGen code ) {} + // X86-64 V2. Includes e.g. SSE4.2 and POPCNT. + @Override public String name() { return "x86_64_v2"; } + @Override public int defaultOpSize() { return 5; } + + public static final int RAX = 0, RCX = 1, RDX = 2, RBX = 3, RSP = 4, RBP = 5, RSI = 6, RDI = 7; + public static final int R08 = 8, R09 = 9, R10 = 10, R11 = 11, R12 = 12, R13 = 13, R14 = 14, R15 = 15; + + public static final int XMM0 = 16, XMM1 = 17, XMM2 = 18, XMM3 = 19, XMM4 = 20, XMM5 = 21, XMM6 = 22, XMM7 = 23; + public static final int XMM8 = 24, XMM9 = 25, XMM10 = 26, XMM11 = 27, XMM12 = 28, XMM13 = 29, XMM14 = 30, XMM15 = 31; + public static final int FLAGS = 32; + public static final int MAX_REG = 33; + public static final int RPC = 33; + + // Human-readable name for a register number, e.g. "RAX". + 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", + "flags", + }; + // Hard crash for bad register number, fix yer bugs! + @Override public String[] regs() { return REGS; } + + public static int XMM_OFFSET = 16; + // General purpose register mask: pointers and ints, not floats + static final long RD_BITS = 0b1111111111111111L; // All the GPRs + static RegMask RMASK = new RegMask(RD_BITS); + // No RSP in the *write* general set. + static final long WR_BITS = 0b1111111111101111L; // All the GPRs minus RSP + static RegMask WMASK = new RegMask(WR_BITS); + // Xmm register mask + static final long FP_BITS = 0b1111111111111111L << XMM0; // All the XMMs + static RegMask XMASK = new RegMask(FP_BITS); + static RegMask FLAGS_MASK = new RegMask(FLAGS); + static RegMask RPC_MASK = new RegMask(RPC); + // Divide mask: exclude RDX:RAX + static RegMask DIV_MASK = new RegMask(RD_BITS & ~(1L<<RAX) & ~(1L<<RDX)); + + static final long SPILLS = -(1L << MAX_REG); + static final RegMask SPLIT_MASK = new RegMask(WR_BITS | FP_BITS | SPILLS, -1L ); + + // Load/store mask; both GPR and FPR + static RegMask MEM_MASK = new RegMask(WR_BITS | FP_BITS); + + public static RegMask RAX_MASK = new RegMask(RAX); + public static RegMask RCX_MASK = new RegMask(RCX); + public static RegMask RDX_MASK = new RegMask(RDX); + public static RegMask RDI_MASK = new RegMask(RDI); + public static RegMask R08_MASK = new RegMask(R08); + public static RegMask R09_MASK = new RegMask(R09); + public static RegMask RSI_MASK = new RegMask(RSI); + + public static RegMask XMM0_MASK = new RegMask(XMM0); + + // Encoding + public static int REX = 0x40; + public static int REX_W = 0x48; + public static int REX_WR = 0x4C; + public static int REX_WRB= 0x4D; + public static int REX_WB = 0x49; + + // opcode included here + // 0F 84 cd JE rel32 + // 0F 85 cd JNE rel32 + // 0F 8F cd JG rel32 + // 0F 8C cd JL rel32 + // 0F 8E cd JLE rel32 + // 0F 8D cd JGE rel32 + static public int jumpop(String op) { + return switch (op) { + case "==" -> 0x84; + case "!=" -> 0x85; + case ">" -> 0x8F; + case "<" -> 0x8C; + case "<=" -> 0x8E; + case ">=" -> 0x8D; + default -> throw new IllegalArgumentException("Too many arguments"); + }; + } + + // Limit of float args passed in registers + static RegMask[] XMMS8 = new RegMask[]{ + new RegMask(XMM0), new RegMask(XMM1), new RegMask(XMM2), new RegMask(XMM3), + new RegMask(XMM4), new RegMask(XMM5), new RegMask(XMM6), new RegMask(XMM7), + }; + + // Map from function signature and argument index to register. + // Used to set input registers to CallNodes, and ParmNode outputs. + @Override public RegMask callArgMask( TypeFunPtr tfp, int idx, int maxArgSlot ) { return callInMask(tfp,idx,maxArgSlot); } + @Override public RegMask callArgMask( TypeFunPtr tfp, int idx, int maxArgSlot, String conv ) { return callInMask(tfp,idx,maxArgSlot,conv); } + static RegMask callInMask( TypeFunPtr tfp, int idx, int maxArgSlot ) { + return callInMask(tfp,idx,maxArgSlot,CodeGen.CODE._callingConv); + } + static RegMask callInMask( TypeFunPtr tfp, int idx, int maxArgSlot, String conv ) { + if( idx==0 ) return RPC_MASK; + if( idx==1 ) return null; + if( idx-2 >= tfp.nargs() ) return null; // Anti-dependence + return switch( conv ) { + case "SystemV" -> callSys5 (tfp,idx,maxArgSlot); + case "win64" -> callWin64(tfp,idx,maxArgSlot); + default -> throw Utils.TODO(); + }; + } + + // Maximum stack args used by this signature + @Override public short maxArgSlot( TypeFunPtr tfp ) { + return maxArgSlot(tfp,CodeGen.CODE._callingConv); + } + @Override public short maxArgSlot( TypeFunPtr tfp, String conv ) { + return switch( conv ) { + case "SystemV" -> maxArgSlotSys5 (tfp); + case "win64" -> maxArgSlotWin64(tfp); + default -> throw Utils.TODO(); + }; + } + + // Win64 - max *4* args in registers of all kinds; after that on stack. + // Stack args land in increasing memory order with empty/mirror slots for + // every register. + // + // foo( int i0, flt f1, int i2, flt f3, int i4, flt f5, int i6, ... ) + // + // -- Prior Frame -- + // i6 + // f5 + // i4 + // empty mirror, XMM3= f3 + // empty mirror, R08 = i2 + // empty mirror, XMM1= f1 + // empty mirror, RCX = i0 + // -- Caller Frame; 16b align -- + // RPC + // PAD/ALIGN + // -- Callee Frame; 16b align -- + static RegMask[] WIN64_CALL = new RegMask[] { + RCX_MASK, + RDX_MASK, + R08_MASK, + R09_MASK, + }; + + static RegMask callWin64(TypeFunPtr tfp, int idx, int maxArgSlot ) { + // idx 2,3,4,5 passed in registers, with stack slot mirrors. + // idx >= 6 passed on stack, starting at slot#1 (#0 reserved for RPC). + int sigidx = idx-2; + if( hiddenSelf(tfp) ) { + if( sigidx==0 ) return null; + sigidx--; + } + if( sigidx >= 4 ) + return new RegMask(MAX_REG+maxArgSlot+sigidx); + return tfp.arg(idx-2) instanceof TypeFloat + ? XMMS8 [sigidx] + : WIN64_CALL[sigidx]; + } + static short maxArgSlotWin64(TypeFunPtr tfp) { + return (short)Math.max(4,tfp.nargs() - (hiddenSelf(tfp) ? 1 : 0)); + } + static short maxIncomingArgSlotWin64(TypeFunPtr tfp) { + return (short)Math.max(0,tfp.nargs() - (hiddenSelf(tfp) ? 1 : 0) - 4); + } + + // Sys5: max 6 GPRs and 8 FPRS filled first. Extra args land in increasing + // memory order as needed - no mirror space. + // + // foo( int i0, flt f1, int i2, flt f3, int i4, flt f5, int i6, ... ) + // RDI =i0, RSI =i2, RDX =i4, RCX =i6, R08 =i8, R09 =i10 + // XMM0=f1, XMM1=f3, XMM2=f5, XMM3=f7, XMM4=f9, XMM5=f11, XMM6=f13, XMM7=f15 + // + // -- Prior Frame -- + // f18 + // i18 + // f17 + // i16 + // i14 + // i12 + // -- Caller Frame; 16b align -- + // RPC + // PAD/ALIGN + // -- Callee Frame; 16b align -- + // SystemV(param passing) + static RegMask[] SYS5_CALL = new RegMask[] { + RDI_MASK, + RSI_MASK, + RDX_MASK, + RCX_MASK, + R08_MASK, + R09_MASK, + }; + + static RegMask callSys5(TypeFunPtr tfp, int idx, int maxArgSlot ) { + // First 6 integers passed in registers: rdi,rsi,rdx,rcx,r08,r09 + // First 8 floats passed in registers: xmm0-xmm7 + int sigidx = idx-2; + if( hiddenSelf(tfp) ) { + if( sigidx==0 ) return null; + sigidx--; + } + int icnt=0, fcnt=0; // Count of ints, floats + for( int i=hiddenSelf(tfp) ? 1 : 0; i<idx-2; i++ ) { + if( tfp.arg(i) instanceof TypeFloat ) fcnt++; + else icnt++; + } + int nstk = Math.max(icnt-6,0)+Math.max(fcnt-8,0); + return tfp.arg(idx-2) instanceof TypeFloat + ? fcnt<8 ? XMMS8 [fcnt] : new RegMask(MAX_REG+maxArgSlot+nstk) + : icnt<6 ? SYS5_CALL[icnt] : new RegMask(MAX_REG+maxArgSlot+nstk); + } + static short maxArgSlotSys5(TypeFunPtr tfp) { + int icnt=0, fcnt=0; // Count of ints, floats + for( int i=hiddenSelf(tfp) ? 1 : 0; i<tfp.nargs(); i++ ) { + if( tfp.arg(i) instanceof TypeFloat ) fcnt++; + else icnt++; + } + int nstk = Math.max(icnt-6,0)+Math.max(fcnt-8,0); + return (short)nstk; + } + private static boolean hiddenSelf(TypeFunPtr tfp) { + return tfp.nargs() > 0 && tfp.arg(0) == TypePtr.PTR; + } + + static final long SYSTEM5_CALLER_SAVE = + (1L << RAX) | (1L << RCX) | (1L << RDX) | + (1L << RDI) | (1L << RSI) | + (1L << R08) | (1L << R09) | (1L << R10) | (1L << R11) | + (1L << FLAGS) | // Flags are killed + // All FP regs are killed + FP_BITS; + + static final long WIN64_CALLER_SAVE = + (1L<< RAX) | (1L<< RCX) | (1L<< RDX) | + (1L<< R08) | (1L<< R09) | (1L<< R10) | (1L<< R11) | + (1L<<FLAGS)| // Flags are killed + // Only XMM0-XMM5 are killed; XMM6-XMM15 are preserved + (1L<<XMM0) | (1L<<XMM1) | (1L<<XMM2) | (1L<<XMM3) | + (1L<<XMM4) | (1L<<XMM5) ; + + @Override public long callerSave() { + return callerSave(CodeGen.CODE._callingConv); + } + @Override public long callerSave(String conv) { + return switch (conv) { + case "SystemV" -> SYSTEM5_CALLER_SAVE; + case "win64" -> WIN64_CALLER_SAVE; + default -> throw new IllegalArgumentException("Unknown calling convention: " + conv); + }; + } + @Override public String cCallingConv(String callingConv) { + return callingConv; + } + @Override public long neverSave() { return 1L<<RSP; } + @Override public RegMask retMask( TypeFunPtr tfp ) { + return tfp.ret() instanceof TypeFloat ? XMM0_MASK : RAX_MASK; + } + @Override public int rpc() { return RPC; } + + + // Create a split op; any register to any register, including stack slots + @Override public SplitNode split(LRG lrg, String kind, byte round ) { + return lrg._machDef!=null && lrg._machDef.outregmap().test(FLAGS) + ? new SplitFlagsX86(lrg,kind,round) + : new SplitX86(lrg,kind,round); + } + + // Return a MachNode unconditional branch + @Override public CFGNode jump() { + return new UJmpX86(); + } + + // Instruction selection + @Override + public Node instSelect(Node n) { + return switch (n) { + case AddNode add -> add(add); + case AndNode and -> and(and); + case BoolNode bool -> cmp(bool); + case CLInitNode init -> new CLInitNode(init); + case CProjNode c -> new CProjNode(c); + case CallEndNode cend -> new CallEndMach(cend); + case CallNode call -> call(call); + case PtrToIntNode ptr -> new PtrToIntMach(ptr); + case GuardNode guard -> new GuardMach(guard); + case ConstantNode con -> con(con); + case DivNode div -> div.mode()==2 ? new DivFX86(div) : new DivX86(div); + case EscapeNode esc -> new EscapeNode(esc); + case FunNode fun -> new FunX86(fun); + case FunPtrNode fptr -> fptr(fptr); + case IfNode iff -> jmp(iff); + case LoadNode ld -> ld(ld); + case MemMergeNode mem -> new MemMergeNode(mem); + case MinusNode neg -> { + if( neg.mode()==2 ) throw Utils.TODO(); + yield new NegX86(neg); + } + case MulNode mul -> mul.mode()==2 ? new MulFX86(mul) : mul(mul); + case NewNode nnn -> new NewX86(nnn); + case NotNode not -> new NotX86(not); + case OrNode or -> or(or); + case ParmNode parm -> new ParmX86(parm); + case BulkMemPhiNode phi-> new BulkMemPhiNode(phi); + case MemPhiNode phi -> new MemPhiNode(phi); + case PhiNode phi -> new PhiNode(phi); + case ProjNode prj -> prj(prj); + case ReadOnlyNode read-> new ReadOnlyMach(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 StartCUNode start -> new StartCUNode(start); + case StopCUNode stop -> new StopCUNode(stop); + case StartNode start -> new StartNode(start); + case StopNode stop -> new StopNode(stop); + case StoreNode st -> st(st); + case SubNode sub -> sub.mode()==2 ? new SubFX86(sub) : sub(sub); + case ToFloatNode tfn -> i2f8(tfn); + case ToIntegerNode cvt -> new F8ToIX86(cvt); + case XCtrlNode x -> ConstantNode.raw(Type.XCONTROL); + case XorNode xor -> xor(xor); + + case LoopNode loop -> new LoopNode(loop); + case RegionNode region -> new RegionNode(region); + default -> throw Utils.TODO(); + }; + } + + public static boolean imm8( long imm ) { return -128 <= imm && imm <= 127; } + + public static boolean imm32( long imm ) { return (int)imm==imm; } + + // Attempt a full LEA-style break down. + private Node add(AddNode add) { + if( add.mode()==2 ) + return new AddFX86(add); + Node lhs = add.in(1); + Node rhs = add.in(2); + if( lhs instanceof LoadNode ld && ld.nOuts() == 1 && ld.declaredType().log_size() >= 3) + return new AddMemX86(add, address(ld), ld.ptr(), idx, off, scale, 0, rhs); + // Attempt a full LEA-style break down. + // Returns one of AddX86, AddIX86, LeaX86, or LHS + if( rhs instanceof ConstantNode off2 && off2._con instanceof TypeInteger toff ) { + long imm = toff.value(); + assert imm!=0; // Folded in peeps + if( (int)imm != imm ) // Full 64bit immediate + return new AddX86(add); + // Now imm <= 32bits + if( lhs instanceof AddNode ladd ) + // ((base + (idx << scale)) + off) + return _lea(add, ladd.in(1), ladd.in(2), (int)imm); + if( lhs instanceof ShlNode shift ) + // (idx << scale) + off; no base + return _lea(add, null, shift, (int)imm); + + // lhs + rhs1 + return new AddIX86(add, (int)imm); + } + return _lea(add, lhs, rhs, 0); + } + + + private Node _lea(Node add, Node base, Node idx, int off) { + int scale = 0; + 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 = ((int) tscale.value()); + } + // (base + idx) + off + return off == 0 && scale == 0 + ? 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 && imm32(ti.value()) ) + return new AndIX86(and, (int)ti.value()); + return new AndX86(and); + } + + private Node call(CallNode call) { + return call.fptr() instanceof FunPtrNode con + ? new CallX86(call, (TypeFunPtr)con._type) + : call.fptr() instanceof ConstantNode con && con._type instanceof TypeFunPtr + ? new CallX86(call, (TypeFunPtr)con._type) + : 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 static boolean swap, unsigned; + private Node cmp( BoolNode bool ) { + swap = unsigned = false; + Node cmp = _cmp(bool); + return new SetX86(cmp, swap ? IfNode.swap(bool.op()) : bool.op(), unsigned); + } + + private Node _cmp(BoolNode bool) { + // Float variant + if( bool.isFloat() ) { + unsigned = true; + return new CmpFX86(bool); + } + Node lhs = bool.in(1); + Node rhs = bool.in(2); + + // Since cmp does not record a BOP, SetX/Jmp need to know if the CMP is swapped. + + // Vs memory + if( lhs instanceof LoadNode ld && ld.nOuts() == 1 && rhs._type.isa(ld.declaredType()) ) + return new CmpMemX86(bool, address(ld), ld.ptr(), idx, off, scale, imm(rhs), val, false); + + // Operands swap in the encoding directly, no need for Set/Jmp to swap `bop` + if( rhs instanceof LoadNode ld && ld.nOuts() == 1 && lhs._type.isa(ld.declaredType()) && + (!(lhs instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value())) || + bool.op()=="==" || bool.op()=="!=") ) + return new CmpMemX86(bool, address(ld), ld.ptr(), idx, off, scale, imm(lhs), val, true ); + + // Vs immediate + if( rhs instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value()) ) + return new CmpIX86(bool, (int)ti.value(), false); + + // Operand swap compare; Set and Jmp need to swap `bop` + if( lhs instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value()) ) + return new CmpIX86(bool, (int)ti.value(), swap=true); + + // x vs y + return new CmpX86(bool); + } + + private Node con( ConstantNode con ) { + if( !con._con.isConstant() ) + return ConstantNode.raw(con); // Default unknown caller inputs + String ext = con instanceof ExternNode ext0 ? ext0._extern : null; + return switch (con._con) { + case TypeInteger ti -> new IntX86(con,ext); + case TypeFloat tf -> new FltX86(con,ext); + case TypeMemPtr tmp -> new TMPX86(con,ext); + case TypeFunPtr tfp -> new TFPX86(tfp,ext); + case TypeNil tn -> throw Utils.TODO(); + // TOP, BOTTOM, XCtrl, Ctrl, etc. Never any executable code. + case Type t -> t == Type.NIL ? new IntX86(con,null) : ConstantNode.raw(con); + }; + } + + private Node fptr( FunPtrNode con ) { + return new TFPX86(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 + String op = "!="; + if( iff.in(1) instanceof BoolNode bool ) op = swap ? IfNode.swap(bool.op()) : bool.op(); + else if( iff.in(1)==null ) op = "=="; // Never-node cutout + else iff.setDef(1, new BoolNode.NE(iff.in(1), + ConstantNode.raw(TypeInteger.ZERO), + (byte)(iff.in(1)._type instanceof TypeFloat ? 2 : 1))); + return new JmpX86(iff, op); + } + + private Node ld(LoadNode ld) { + return new LoadX86(address(ld), ld.ptr(), idx, off, scale); + } + + private Node mul(MulNode mul) { + if( mul.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value()) ) + return new MulIX86(mul, (int)ti.value()); + return new MulX86(mul); + } + + private Node or(OrNode or) { + if( or.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value()) ) + return new OrIX86(or, (int)ti.value()); + return new OrX86(or); + } + + private Node prj( ProjNode prj ) { + if( prj.in(0) instanceof CLInitNode ) return new ProjNode(prj); + TypeInteger ret = cReturn(prj); + if( ret!=null ) return new ExtendX86(prj,ret); + return prj.in(0) instanceof StartNode ? new ProjNode(prj) : new ProjX86(prj); + } + + private Node sar(SarNode sar) { + if( sar.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm8(ti.value())) + return new SarIX86(sar, (int)(ti.value() & 0xff) ); + return new SarX86(sar); + } + + private Node shl(ShlNode shl) { + if( shl.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm8(ti.value())) + return new ShlIX86(shl, (int)(ti.value() & 0xff) ); + return new ShlX86(shl); + } + + private Node shr( ShrNode shr ) { + if( shr.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm8(ti.value()) ) + return new ShrIX86(shr, (int)(ti.value() & 0xff) ); + return new ShrX86(shr); + } + + 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 && stld_match(st,ld) ) + return new MemAddX86(address(st), st.ptr(), idx, off, scale, imm(op.in(2)), val); + if( op.in(2) instanceof LoadNode ld && stld_match(st,ld) ) + return new MemAddX86(address(st), st.ptr(), idx, off, scale, imm(op.in(1)), val); + } + return new StoreX86(address(st), st.ptr(), idx, off, scale, imm(st.val()), val); + } + private static boolean stld_match(StoreNode st, LoadNode ld ) { + return + ld.in(0) == st.in(0) && + ld.mem() == st.mem() && + ld.ptr() == st.ptr() && + ld.off() == st.off(); + } + + + private Node sub (SubNode sub ){ + return sub.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti + ? new AddIX86(sub, (int)-ti.value()) + : new SubX86(sub); + } + + private Node xor (XorNode xor){ + if( xor.in(2) instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value()) ) + return new XorIX86(xor, (int)ti.value()); + return new XorX86(xor); + } + + // 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 && ti.isConstant() && imm32(ti.value()) ) { + val = null; + imm = (int) ti.value(); + assert imm == ti.value(); // In 32-bit range + } else { + val = xval; + } + return imm; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java b/chapter-modules/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java new file mode 100644 index 00000000..58506e9c --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java @@ -0,0 +1,101 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.print.AssemblyAdapter; +import com.seaofnodes.print.ASMPrinter.Data; +import com.seaofnodes.print.ASMPrinter.Pool; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.BAOS; +import java.util.ArrayList; +import com.seaofnodes.simple.codegen.Encoding; +import java.util.HashMap; + +/** Machine-specific facts; traversal, columns and byte display live in print/. */ +public final class ASMPrinter extends AssemblyAdapter<Node> { + private final CodeGen _code; + private final Encoding _enc; + private ASMPrinter(CodeGen code) { + super(new IRPrinter()); + _code=code; + _enc=code._encoding; + } + public static SB print(SB sb, CodeGen code) { + return sb.p(new com.seaofnodes.print.ASMPrinter<>(new ASMPrinter(code)).print()); + } + @Override public ArrayList<Node> blocks() { + var blocks=new ArrayList<Node>(); + var cfg=_enc==null ? _code._cfg : _enc._cfg; + if( cfg!=null ) for( Node n : cfg ) blocks.add(n); + return blocks; + } + @Override public String op(Node n) { return n instanceof MachNode m ? m.op() : n.label(); } + @Override public String register(Node n) { return _code.reg(n); } + @Override public String comment(Node n) { return n.comment(); } + @Override public String phiName(Node n) { return ((PhiNode)n)._label; } + @Override public String operands(Node n) { + SB sb=new SB(); + if( n instanceof MachNode m ) m.asm(_code,sb); + else if( !(n._type instanceof TypeMem) ) { + sb.p(n._nid).p(": "); + for( Node def : n._inputs ) sb.p(def==null ? "___" : Integer.toString(def._nid)).p(' '); + } + return sb.toString(); + } + @Override public boolean labelBlock(Node n) { + return !(n instanceof FunNode) && !(n instanceof IfNode) && !(n instanceof CallNode) && + !(n instanceof CallEndNode) && !(n instanceof CProjNode && ir.input0(n) instanceof CallEndNode); + } + @Override public boolean hidden(Node n) { + if( n instanceof PhiNode || n instanceof ProjNode ) + if( n._type instanceof TypeMem || n._type instanceof TypeRPC ) return true; + if( postAlloc() && n instanceof CalleeSaveNode ) return true; + if( n instanceof MemMergeNode ) return true; + if( n instanceof EscapeNode ) return true; + return n.getClass()==ConstantNode.class; + } + @Override public boolean postAlloc() { return _code._phase.ordinal()>CodeGen.Phase.RegAlloc.ordinal() || + (_code._phase==CodeGen.Phase.RegAlloc && _code._regAlloc!=null && _code._regAlloc.done()); } + @Override public boolean encoded() { return _enc!=null && _code._phase.ordinal()>=CodeGen.Phase.Encoding.ordinal(); } + @Override public int defaultSize() { return _code._mach==null ? 2 : _code._mach.defaultOpSize(); } + @Override public boolean littleEndian() { return _code._asmLittle; } + @Override public boolean prologue(Node n) { return ((FunNode)n)._frameAdjust!=0; } + @Override public byte[] bytes() { return encoded() ? _enc._bits.buf() : null; } + @Override public int offset(Node n) { return encoded() ? _enc.opStart(n) : -1; } + @Override public int size(Node n) { return encoded() ? _enc.opLen(n)&255 : 0; } + private static ArrayList<Data> entries(Iterable<Encoding.Relo> relos, boolean ro) { + var data=new ArrayList<Data>(); + for( Encoding.Relo r : relos ) { + if( r.readOnly()!=ro ) continue; + int align=1<<r._align; + int size=r._t instanceof TypeStruct ? (r._structSize+align-1)&-align : align; + data.add(new Data(r._t,r._t.str(),r._align,size)); + } + return data; + } + @Override public ArrayList<Pool> pools(int codeEnd) { + var pools=new ArrayList<Pool>(); + if( !encoded() ) return pools; + int base=(codeEnd+15)&-16; + if( _enc._cpool.size()>0 ) + pools.add(new Pool("Constant Pool",_enc._cpool.buf(),0,base,objects(true))); + base=(base+_enc._cpool.size()+15)&-16; + if( _enc._sdata.size()>0 ) + pools.add(new Pool("Static Data",_enc._sdata.buf(),0,base,objects(false))); + return pools; + } + private ArrayList<Data> objects(boolean ro) { + var data=new ArrayList<Data>(); + for( var obj : _enc._data.entries() ) + if( !obj.external && obj.readOnly==ro ) + data.add(new Data(obj,obj.symbol+": "+obj.type.str(),obj.alignment,obj.size)); + return data; + } + // Retain this diagnostic entry point for checking that pool printing never lays out types. + static int printConstantPool(int address, SB sb, BAOS bits, HashMap<Node,Encoding.Relo> relos, + boolean ro, String name) { + sb.p(com.seaofnodes.print.ASMPrinter.pool(new Pool(name,bits.buf(),0,address,entries(relos.values(),ro)))); + return address+bits.size(); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/print/IRPrinter.java b/chapter-modules/src/main/java/com/seaofnodes/simple/print/IRPrinter.java new file mode 100644 index 00000000..531a549a --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/print/IRPrinter.java @@ -0,0 +1,60 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.print.IRAdapter; +import com.seaofnodes.simple.node.*; +import java.util.ArrayList; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.codegen.CodeGen; + +/** This chapter's read-only IR facts and the familiar debugger entry points. */ +public final class IRPrinter extends IRAdapter<Node> { + private static final com.seaofnodes.print.IRPrinter<Node> PRINT = + new com.seaofnodes.print.IRPrinter<>(new IRPrinter()); + @Override public boolean global(Node n) { return n.isConst(); } + @Override public String type(Node n) { return n._type==null ? "" : n._type.str(); } + @Override public String inputMark(Node n, Node def) { return n instanceof MemMergeNode && def instanceof MemMergeNode ? "^" : " "; } + @Override public Kind kind(Node n) { + if( n instanceof StartCUNode ) return Kind.UNIT; + if( n instanceof StopCUNode ) return Kind.UNIT; + if( n instanceof StartNode ) return Kind.START; + if( n instanceof StopNode ) return Kind.STOP; + if( n instanceof FunNode ) return Kind.FUN; + if( n instanceof LoopNode ) return Kind.LOOP; + if( n instanceof RegionNode ) return Kind.REGION; + if( n instanceof ParmNode ) return Kind.PARM; + if( n instanceof PhiNode ) return Kind.PHI; + if( n instanceof CallEndNode ) return Kind.CALL_END; + if( n instanceof CallNode ) return Kind.CALL; + if( n instanceof ReturnNode ) return Kind.RETURN; + if( n instanceof ConstantNode ) return Kind.CONSTANT; + if( n instanceof CProjNode ) return Kind.CPROJ; + if( n instanceof ProjNode ) return n instanceof CFGNode ? Kind.CPROJ : Kind.PROJ; + if( n instanceof CFGNode ) return Kind.CTRL; + if( n instanceof MultiNode ) return Kind.MULTI; + return Kind.DATA; + } + @Override public int index(Node n) { + if( n instanceof ProjNode p ) return p._idx; + if( n instanceof CProjNode p ) return p._idx; + return n._nid; + } + @Override public String functionName(Node n) { return ((FunNode)n)._name==null ? "" : ((FunNode)n)._name; } + @Override public String signature(Node n) { + FunNode fun=(FunNode)n; + return (fun._name==null ? "" : fun._name)+" "+fun.sig().str(); + } + + public static String prettyPrint(Node n, int depth) { return PRINT.prettyPrint(n,depth); } + public static SB printLine(Node n, SB sb) { return sb.p(PRINT.line(n)); } + + public static String prettyPrint(CodeGen code) { + var units=new ArrayList<com.seaofnodes.print.IRPrinter.Unit<Node>>(); + for( var cu : code._compunits.values() ) { + var funs=new ArrayList<Node>(); + for( FunNode fun : code._linker ) + if( fun!=null && fun._compunit==cu ) funs.add(fun); + units.add(new com.seaofnodes.print.IRPrinter.Unit<>(cu._cname,cu._start,cu._stop,funs)); + } + return PRINT.program(code._start,code._stop,units); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java new file mode 100644 index 00000000..9ca0ec51 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -0,0 +1,80 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.graph.GraphAdapter; +import com.seaofnodes.graph.GraphSnapshot.*; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeMem; + +/** Chapter 25's view of the IR, including partially constructed parse-time graphs. */ +public class SimpleGraphAdapter extends GraphAdapter<Node> { + + @Override protected boolean show(Node n) { + if( n != CodeGen.CODE.ZERO && n != CodeGen.CODE.XCTRL ) return true; + for( int i=0; i<n.nOuts(); i++ ) + if( n.out(i) != null ) return true; + return false; // The parser's keep is not a graph use. + } + + @Override protected String edgeName(Node n, int i) { + return n instanceof ScopeNode scope && i < scope._vars.size() + ? scope.var(i)._name : null; + } + @Override protected int edgeJump(Node n, int i) { + return n instanceof CallEndNode && i>0 && n.in(i) instanceof ReturnNode ret + ? ref(ret.fun()) : 0; + } + @Override protected int projectionIndex(Node n) { return n instanceof Proj p ? p.idx() : -1; } + + @Override protected boolean folding(Node n) { + if( n instanceof FunNode fun ) return fun.folding(); + if( n instanceof ReturnNode ret ) return ret.fun().folding(); + if( n instanceof CallEndNode cend ) return cend.folding(); + if( n instanceof CallNode ) + for( int i=0; i<n.nOuts(); i++ ) + if( n.out(i) instanceof CallEndNode cend && cend.folding() ) return true; + return false; + } + + @Override protected Kind kind(Node n) { + if( n instanceof StopNode ) return Kind.STOP; + if( n instanceof ScopeNode ) return Kind.SCOPE; + if( n instanceof StartCUNode ) return Kind.UNIT; + if( n instanceof FunNode ) return Kind.FUN; + // Start inherits Loop for whole-program analysis, but is not a source loop. + if( n instanceof StartNode ) return Kind.START; + if( n instanceof LoopNode ) return Kind.LOOP; + if( n instanceof RegionNode ) return Kind.REGION; + if( n instanceof PhiNode ) return Kind.PHI; + if( n instanceof CFGNode ) return Kind.CTRL; + return isMem(n) ? Kind.MEM : Kind.DATA; + } + + @Override protected Role role(Node n, int i) { + if( n instanceof ScopeNode || n instanceof ConstantNode || n instanceof FunPtrNode || + n instanceof CtrlNode || n instanceof XCtrlNode ) + return Role.ASSOC; + if( n instanceof PhiNode ) + return i == 0 ? Role.ASSOC : isMem(n) ? Role.MEM : Role.DATA; + if( n instanceof Proj ) + return n instanceof CFGNode ? Role.CTRL : isMem(n) ? Role.MEM : Role.DATA; + // StartCU inherits Region, but slot 0 really connects it to Start. + if( n instanceof StartCUNode && i == 0 ) return Role.CTRL; + if( n instanceof RegionNode && i == 0 ) return Role.ASSOC; + // Preserve known slot roles even when an input is not attached/typed yet. + if( i == 0 || n instanceof RegionNode || n instanceof StopNode ) return Role.CTRL; + if( n instanceof MemMergeNode || + (i == 1 && (n instanceof MemOpNode || n instanceof ReturnNode || n instanceof CallNode)) || + (n instanceof EscapeNode && i >= 2) ) return Role.MEM; + Node def = n.in(i); + if( def instanceof CFGNode ) return Role.CTRL; + if( def != null && isMem(def) ) return Role.MEM; + return Role.DATA; + } + + private boolean isMem(Node n) { + // These Phi subclasses are memory even before their first type computation. + return n instanceof MemPhiNode || n instanceof BulkMemPhiNode || n.isMem() || n._type instanceof TypeMem; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java new file mode 100644 index 00000000..f2ed2f72 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -0,0 +1,35 @@ +package com.seaofnodes.simple.print; + +import com.seaofnodes.graph.GraphCapture; +import com.seaofnodes.graph.GraphViewer; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.Node; +import java.util.ArrayList; +import java.io.IOException; + +/** Chapter 25's compilation context; capture and event bookkeeping are shared. */ +public class SimpleGraphObserver extends GraphCapture<Node> { + private CodeGen _code; + public SimpleGraphObserver() { super(new SimpleGraphAdapter()); } + public static void main(String[] args) throws IOException { + GraphViewer.run(new SimpleGraphObserver()); + } + @Override protected void compile(String src) { + _code = new CodeGen(src); + _code._obs = this; + _code.driver(CodeGen.Phase.TypeCheck); + } + @Override protected String phase() { return _code._phase.name(); } + @Override protected int pos() { return _code._phase == CodeGen.Phase.Parse ? _code.P.pos() : -1; } + @Override protected void detach() { + if( _code != null ) _code._obs = null; + _code = null; + } + @Override protected Node scope() { return _code.P._scope; } + @Override protected void roots(ArrayList<Node> roots) { + roots.add(_code._stop); + if( _code._phase == CodeGen.Phase.Parse ) { + roots.addAll(_code.P._xScopes); + } + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/sys.java b/chapter-modules/src/main/java/com/seaofnodes/simple/sys.java new file mode 100644 index 00000000..45106770 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/sys.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + + +/** Default included namespaces for Simple. + + You can think of this as {@code #include <stdio.h>} for C or {@code import java.lang.*} for Java. + */ +public abstract class sys { + + public static final String SYS; + + static { + try { + SYS = Files.readString(Path.of("src/main/smp/sys.smp")); + } + catch( Exception e ) { + throw new RuntimeException(e); + } + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/Field.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/Field.java new file mode 100644 index 00000000..ec4a588d --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/Field.java @@ -0,0 +1,142 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.SB; +import java.util.*; + +/** + * Represents a field in a struct. This is not a Type in the type system. + */ +public class Field extends Type { + static final Field[] EMPTY = new Field[0]; + + // The pair {fieldName,type} uniquely identifies a field. + + // Field name + public String _fname; + // Type of the field + public Type _t; + // Unique memory alias, not sensibly part of a "type" but very convenient here. + public int _alias; + // Field is only written in the constructor + public boolean _final; + // Native C storage, outside the containing object's layout. + public boolean _extern; + + private static final Ary<Field> FREE = new Ary<>(Field.class); + private Field(String fname, Type type, int alias, boolean xfinal, boolean external ) { super(TFLD); init(fname,type,alias,xfinal,external); } + private Field init(String fname, Type type, int alias, boolean xfinal, boolean external ) { + _fname = fname; + _t = type; + _alias = alias; + _final = xfinal; + _extern = external; + return this; + } + + // Return a filled-in Field; either from free list or alloc new. + private static Field malloc(String fname, Type type, int alias, boolean xfinal, boolean external ) { + return FREE.isEmpty() ? new Field(fname,type,alias,xfinal,external) : FREE.pop().init(fname,type,alias,xfinal,external); + } + // Malloc-from + public Field malloc( ) { return malloc(_fname,null,_alias,_final,_extern); } + @Override Field free(Type t) { + Field f = (Field)t; + assert !f.isFree() && !f._terned; + f._t = null; + f._dual = null; + f._hash = 0; + FREE.push(f); + return this; + } + @Override boolean isFree() { return _t ==null; } + + // Make and intern with listed fields + public static Field make( String fname, Type type, int alias, boolean xfinal ) { + return make(fname,type,alias,xfinal,false); + } + public static Field make( String fname, Type type, int alias, boolean xfinal, boolean external ) { + Field f = malloc(fname,type,alias,xfinal,external); + Field f2 = f.intern(); + if( f2==f ) return f; + return VISIT.isEmpty() ? f2.free(f) : f2.delayFree(f); + } + public Field makeFrom( Type type ) { + return type == _t ? this : make(_fname,type,_alias,_final,_extern); + } + public Field makeFrom( boolean xfinal ) { + return xfinal == _final ? this : make(_fname,_t,_alias,xfinal,_extern); + } + + // Cycle-making-breaking + void setType(Type t) { assert _t ==null; _t = t; } + + public static final Field BOT = make(" ",Type.BOTTOM,-999,false); + public static final Field TEST = make("test",Type.NIL,-2,false); + public static final Field TEST2 = make("test",Type.NIL,-2,true); + public static final Field FLT = make("flt ",TypeFloat.F32,-2,false); + public static final Field EXT = make("test",TypeInteger.I32,-2,false,true); + public static void gather(ArrayList<Type> ts) { ts.add(TEST); ts.add(TEST2); ts.add(FLT); ts.add(EXT); } + + @Override Field xmeet( Type that ) { + Field fld = (Field)that; // Invariant + if( this==BOT || fld==BOT ) return BOT; + if( this==BOT.dual() ) return fld ; + if( fld ==BOT.dual() ) return this; + if( _fname!=fld._fname ) { assert !_fname.equals(fld._fname); return BOT; } + assert _alias==fld._alias; + if( _extern!=fld._extern ) return BOT; + return make(_fname, _t.meet(fld._t ), _alias, _final | fld._final,_extern); + } + + @Override + Field xdual() { return malloc(_fname, _t.dual(),_alias,!_final,_extern); } + + @Override Field rdual() { + if( _dual!=null ) return dual(); + assert !_terned; + Field d = malloc(_fname,null,_alias,!_final,_extern); + (_dual = d)._dual = this; // Cross link duals + d._t = _t._terned ? _t.dual() : _t.rdual(); + return d; + } + + @Override boolean _isConstant() { return _t._isConstant(); } + @Override boolean _isFinal() { return _final && _t._isFinal(); } + @Override Field _makeRO() { return _final ? this : make(_fname, _t._makeRO(),_alias,true,_extern); } + @Override Field _close( String name, HashMap<String, Type> TYPES ) { return makeFrom( _t._close(name, TYPES )); } + + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + return makeFrom(_t._upgradeType(TYPES)); + } + + // Override in subclasses + int hash() { return _fname.hashCode() ^ _t.hashCode() ^ _alias ^ (_final ? 1024 : 0) ^ (_extern ? 2048 : 0); } + + private boolean static_eq( Field f ) { + return _fname.equals(f._fname) && _alias==f._alias && _final==f._final && _extern==f._extern && _t !=null && f._t !=null; + } + @Override boolean eq( Type t ) { + Field f = (Field)t; + return static_eq(f) && _t ==f._t; + } + @Override boolean cycle_eq(Type t) { + if( this==t ) return true; + Field f = (Field)t; + return static_eq(f) && _t.cycle_eq(f._t ); + } + + @Override public int nkids() { return 1; } + @Override public Type at( int idx ) { return _t; } + @Override public void set( int idx, Type t ) { _t = t; } + // Tags: final/!final; +alias+name+external-storage flag + + + @Override + public SB _print( SB sb, BitSet visit, boolean html ) { + sb.p(_extern ? "extern C " : "").p(_final?"":"!").p(_fname).p(":").p(_alias).p(" : "); + return _t ==null ? sb.p("---") : _t.print(sb,visit,html); + } + + @Override public String str() { return (_final?"":"!")+_fname; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/Type.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/Type.java new file mode 100644 index 00000000..1979368e --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/Type.java @@ -0,0 +1,548 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.*; +import java.util.*; + +/** + * 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 /*implements Cloneable*/ { + // Interning tables + + // Main intern table; a collection of all unique types. + static final HashMap<Type,Type> INTERN = new HashMap<>(); + // Starting intern table; used to rapidly reset the main table between tests + private static final HashMap<Type,Type> INTERN0= new HashMap<>(); + // Lifetime management during cyclic installs is complex; this is used to + // track created-but-already-interned objects + private static final Ary<Type> FREES = new Ary<>(Type.class); + // "visit" bit management, based solely on _uid + static final BitSet BITS = new BitSet(); + + // "visit" bit management. Key varies by kind of visit: + + // - "close" an open cyclic type. Used by the Parser, it will hit on + // same-named structs, even if the structs have different states of + // completion/fields. + // - "uid/name" - Either the UID or name works (unlike "close" which must + // use the name). Used to make cyclic copies with changes, like make + // read-only where there should be only 1 copy of a name in a cycle. + // - "pair of uids" - For recursive MEET, where we need to key off two types + static final HashMap<Object,Type> VISIT = new HashMap<>(); + + // Every Type has a UID to allow visit bits on cyclic visits + private static int UID=1; + public char _uid; // Unique ID for every type + + // ---------------------------------------------------------- + // 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 = 5; // End of the Simple Types + + static final byte TSCALAR = 6; // Integer, float, memory pointer or function pointer + static final byte TPTR = 7; // All nil-able pointer values + static final byte TINT = 8; // All Integers; see TypeInteger + static final byte TFLT = 9; // All Floats ; see TypeFloat + static final byte TCONARY =10; // Constant array + static final byte TRPC =11; // Return Program Control (Return PC or RPC) + + static final byte TCYCLIC =12; // Has internal pointers, needs recursive treatment + static final byte TMEMPTR =12; // Memory pointer to a struct type + static final byte TFUNPTR =13; // Function pointer; unique signature and code address (just a bit) + static final byte TMEM =14; // All memory (alias 0) or A slice of memory - with specific alias + static final byte TFLD =15; // Named fields in structs + static final byte TSTRUCT =16; // Structs; tuples with named fields + static final byte TTUPLE =17; // Tuples; finite collections of unrelated Types, kept in parallel + + static final byte TMAX =18; + + // Basic RTTI, useful for a lot of fast tests. + public final byte _type; + public boolean _terned; + + public boolean is_simple() { return _type <= TSIMPLE; } + public boolean is_nokids() { return _type < TCYCLIC; } + private static final String[] STRS = new String[]{"Bot","Top","Ctrl","~Ctrl","null","~nil"}; + protected Type(byte type) { + _type = type; // RTTI + _uid = (char)UID++; // A unique ID for every type + if( UID > 32000 ) + type = type; + assert _uid!=0; // Overflow + } + + public static final Type BOTTOM = new Type( TBOT ).intern(); // ALL + public static final Type TOP = BOTTOM.dual(); + public static final Type CONTROL = new Type( TCTRL ).intern(); // Ctrl + public static final Type XCONTROL = CONTROL.dual(); + public static final Type NIL = new Type( TNIL ).intern(); // low null of all flavors + public static final Type XNIL = NIL.dual(); + public static Type[] gather() { + ArrayList<Type> ts = new ArrayList<>(); + ts.add(BOTTOM); + ts.add(CONTROL); + ts.add(NIL); + TypeScalar.gather(ts); + 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); + TypeConAry.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(); } + + // ---------------------------------------------------------- + + // 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" + @SuppressWarnings("unchecked") + public <T extends Type> T intern() { + //assert check(); + assert !_terned; // Do not ask for already-interned + T t2 = (T)INTERN.get(this); + if( t2!=null ) { + assert t2._dual != null; // Prior is complete with dual + assert this != t2; // Do not hashcons twice, should not get self back + return t2; // Return prior + } + // Mid recursive type construction + if( !VISIT.isEmpty() && _type >= TCYCLIC ) { + // No intern attempt at all + return (T)this; + } + // Not in type table + _dual = null; // No dual yet + INTERN.put(this,this); // Put in table without dual + _terned = true; + //Util.hash_quality_check_per(INTERN,"INTERN"); + T d = xdual(); // Compute dual without requiring table lookup, and not setting name + _dual = d; + if( this==d ) return d; // Self-symmetric? Dual is self + assert !equals(d); // Self-symmetric is handled by caller + assert d._dual==null; // Else dual-dual not computed yet + assert INTERN.get(d)==null; + d._dual = (T)this; + INTERN.put(d,d); // Install dual also + d._terned = true; + //assert check(); + return (T)this; + } + + // Recursive visit elsewhere, interns with dual already + <T extends Type> T _intern() { + //assert check(); + assert !_terned; + Type t = INTERN.get(this); + if( t!=null ) return (T)t.delayFree(this).delayFree(_dual); + assert !INTERN.containsKey( _dual ); + INTERN.put(this,this); + INTERN.put(_dual,_dual); + _terned = _dual._terned = true; + //assert check(); + return (T)this; + } + + private static boolean check() { + for( Type t : INTERN.keySet() ) { + Type t2 = INTERN.get(t); + assert t==t2; + } + return true; + } + + public static Type find(int uid) { + for( Type t : INTERN.keySet() ) + if( t._uid==uid ) return t; + return null; + } + + + 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 this==t; } + boolean cycle_eq(Type t) { assert is_nokids(); return _type==t._type && eq(t); } + // A pair of uids + int pid( Type that ) { + return _uid < that._uid ? (_uid<<16 | that._uid) : (that._uid<<16 | _uid); + } + + // At/Set child at 'idx' to t + public Type at( int idx ) { throw Utils.TODO("Should not reach here: type has no children"); } + public void set( int idx, Type t ) { throw Utils.TODO("Should not reach here: type has no children"); } + public int nkids() { return 0; } // Number of kids + + + // Clear and re-insert the basic Type INTERN table + public static void reset() { + VISIT.clear(); + FREES.clear(); + if( INTERN0.isEmpty() ) { + // First time run all <clinit> and collect basic types + Type t = TypeRPC .BOT; // Force class load, intern + Type u = TypeTuple.BOT; // Force class load, intern + Type w = TypePtr .PTR; // Force class load, intern + INTERN0.putAll(INTERN); + } else { + // Later times, reset back to first time + VISIT.put(0,CONTROL); // Defeat assert + for( Iterator<Type> it = INTERN.keySet().iterator(); it.hasNext(); ) { + Type t = it.next(); + if( !INTERN0.containsKey(t) ) + { t._terned=false; t.free(t); } + it.remove(); + } + VISIT.clear(); + INTERN.putAll(INTERN0); + } + } + + // ---------------------------------------------------------- + public final Type meet(Type t) { + assert _terned && t._terned; + // 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); + // TypeConAry meet its element type is OK + if( this instanceof TypeConAry tcon && t instanceof TypeInteger ti ) return tcon.ymeet(ti); + if( t instanceof TypeConAry tcon && this instanceof TypeInteger ti ) return tcon.ymeet(ti); + // Two distinct scalar families meet at the scalar envelope. + if( this instanceof TypeScalar scalar0 && t instanceof TypeScalar scalar1 ) + return scalar0.smeet(scalar1); + // Mixing 2 unrelated types + return Type.BOTTOM; + } + + // 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 || _type==TXNIL ) { + if( t instanceof TypeNil ptr ) return _type==TNIL ? ptr.meet0() : ptr.meetX(); + if( t instanceof TypeScalar scalar ) { + if( t._type==TSCALAR ) return t.isHigh() ? this : t; + return TypeScalar.BOT; + } + return t._type==(_type==TNIL ? TXNIL : 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; + } + + + // The dual is pre-computed on creation and available "for free" thereafter + Type _dual; + public final <T extends Type> T dual() { return (T)_dual; } + + <T extends Type> T xdual() { + return (T)new Type(switch( _type ) { + case TBOT -> TTOP; + case TTOP -> TBOT; + case TCTRL -> TXCTRL; + case TXCTRL-> TCTRL; + case TNIL -> TXNIL; + case TXNIL -> TNIL; + default -> throw Utils.TODO("Should not reach here: unknown simple type"); + }); + } + + + // ---------------------------------------------------------- + // 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; } + + public Type nonZero() { return this==NIL ? TypePtr.XNPTR : TypePtr.NPTR; } + + // Make a zero version of this type, 0 for integers and null for pointers. + public Type makeZero() { return Type.NIL; } + + // Cap at limits + public Type oob() { return isHigh() ? TOP : BOTTOM; } + + // ---------------------------------------------------------- + + // Cyclic types! Flag the start of a cyclic type by putting a sentinel in + // VISIT (and eventually clearing VISIT when done). Then do a normal + // recursive descent visit of all types; this will accumulate types without + // interning them. When done we have to visit the possibly cyclic type and + // intern the whole cycle, possibly hitting the entire cycle on a prior + // interned cycle. Any sub-part might also be interned, including whole + // disjoint cycles. + + // E.g. a new type: "A<->B -> C1<->D1" gets created; the "C1<->D1" cycle + // already exists as "C0<->D0", but the "A<->B" cycle is new. The "A<->B" + // cycle will get interned - except the pointer to C1 needs to get replaced + // with C0. The "C1<->D1" cycle gets freed and "B" will point to the + // "C0<->D0" cycle. + + Type recurOpen() { assert VISIT.isEmpty(); VISIT.put(0L,BOTTOM); return this; } + Type recurClose() { return recurClose(null); } + Type recurClose(Type[] types) { + VISIT.remove(0L); // Just ignore the sentinel + Ary<Type> ts = new Ary<>(Type.class); + ts.addAll(VISIT.values()); + if( ts.find(this)== -1 ) ts.add(this); + + // Pass #1: replace already interned fields + assert BITS.isEmpty(); + for( int i=0; i<ts._len; i++ ) + if( ts.at(i).tern()._terned ) + ts.del(i--); + BITS.clear(); + + // Pass#2: compute recursive duals + for( Type t : ts ) + t.rdual(); + + // Pass#3: Recursive install + for( Type t : ts ) + if( !t._terned && INTERN.get(t)==null ) + t.install(); + // Upgrade the result + Type rez = INTERN.get(this); + assert rez!=null; + if( types!=null ) + for( int i=0; i<types.length; i++ ) + types[i] = INTERN.get(types[i]); + + // Pass#4?: Free up any created-but-already interned + for( Type t : FREES ) + t.rfree(); + FREES.clear(); + VISIT.clear(); + return rez; + } + + // Read-only recursive visits do not *make* cyclic types and so the cleanup + // is much easier. + static boolean recurClose(boolean rez) { VISIT.clear(); return rez; } + + final Type tern() { + if( _terned ) return this; + if( !BITS.get(_uid) ) { + BITS.set(_uid); + int nkids = nkids(); + for( int i=0; i<nkids; i++ ) + set(i,at(i).tern()); + } + Type told = INTERN.get(this); + return told==null ? this : told.delayFree(this); + } + + // Recursive dual. Visit all types recursively. Each visited type is + // *new*, and hits in the intern table, or not. If not, we recursively + // call rdual to get its dual, and cross-link the duals. If hitting, we + // use the intern type and mark the prior type for freeing. + Type rdual() { assert !_terned; return xdual(); } + + // Recursively free type and children + final void rfree() { + if( isFree() ) return; + int nkids = nkids(); + for( int i=0; i<nkids; i++ ) + if( !at(i)._terned ) + at(i).rfree(); + free(this); + } + + Type free(Type free) { + return this; + } + boolean isFree() { return false; } + + final <T extends Type> T delayFree(Type free) { + assert !free._terned; + FREES.push(free); + return (T)this; + } + + final Type install() { + if( this instanceof TypeStruct ) { + Type x = _intern(); // Stop the recursion + if( x!=this ) return x.delayFree(this); + } + int nkids = nkids(); + for( int i=0; i<nkids; i++ ) { + Type kid = at(i); + if( !kid._terned ) { + Type ikid = kid.install(); + if( ikid != kid ) { + set(i,ikid); + _dual.set(i,ikid.dual()); + } + } + } + return this instanceof TypeStruct || _terned ? this : _intern(); + } + + // Strict constant values, things on the lattice centerline. + // Excludes both high and low values + public final boolean isConstant() { return recurClose(recurOpen()._isConstant()); } + boolean _isConstant() { return _type==TNIL; } + + // Are all reachable struct Fields are final? + // Writable array slots are invariant in their element access permissions. + // Otherwise an alias could insert a readonly pointer into a writable slot. + public boolean accessISA(Type dst) { return true; } + + public final boolean isFinal() { return recurClose(recurOpen()._isFinal()); } + boolean _isFinal() { assert is_nokids(); return true; } + + public final Type makeRO() { + if( isFinal() ) return this; + return recurOpen()._makeRO().recurClose(); + } + Type _makeRO() { return this; } + + /** Declared shape used when a value is published into mutable memory. */ + public final Type makeStorage() { + return recurOpen()._makeStorage().recurClose(); + } + Type _makeStorage() { assert is_simple(); return BOTTOM; } + + // Bulk close-over all recursive types + public static TypeStruct[] closeOver(TypeStruct[] ts, HashMap<String,Type> TYPES) { + BOTTOM.recurOpen(); + for( TypeStruct t : ts ) + t._close(null, TYPES); + for( int i=0; i<ts.length; i++ ) { + // Parser only tracks the mutable name of arrays, so the closeOver + // returns that one. However, internal types might have immutable + // versions. + String name = ts[i].isAry() ? (ts[i]._name+"!").intern() : ts[i]._name; + ts[i] = (TypeStruct)VISIT.get(name); + } + BOTTOM.recurClose(ts); + return ts; + } + + Type _close( String name, HashMap<String, Type> TYPES ) { return this; } + public Type widen() { return this; } + + // Replace recursively all TypeBuilders with cyclic TypeStructs + public Type upgradeType(HashMap<String,Type> TYPES) { + return recurOpen()._upgradeType(TYPES).recurClose(); + } + Type _upgradeType(HashMap<String,Type> TYPES) { assert is_nokids(); 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 () { return 3; } // log-size of a type; log_size for a struct is usually undefined + public int size() { return 1<<log_size(); } + public int alignment() { return log_size(); } // alignment; for structs, max align of Fields + + // ---------------------------------------------------------- + // 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(), new BitSet(), false).toString(); + } + public final String gprint() { + return print(new SB(), new BitSet(), true).toString(); + } + public final SB print(SB sb) { return print(sb, new BitSet(), false); } + public final SB gprint(SB sb) { return print(sb, new BitSet(), true ); } + + // Used during recursive printing, or combined with SB Node printing + public final SB print(SB sb, BitSet visit, boolean html ) { + if( visit.get(_uid) ) return sb.p(str()); + visit.set(_uid); + return _print(sb,visit,html); + } + SB _print(SB sb, BitSet visit, boolean html ) { return sb.p(str()); } + + // This is used by error messages, and is a shorted print. + public String str() { return STRS[_type]; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAry.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAry.java new file mode 100644 index 00000000..860cefc5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAry.java @@ -0,0 +1,80 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.BAOS; +import com.seaofnodes.simple.util.Utils; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Represents a constant array of primitives + */ +public abstract class TypeConAry<A> extends TypeScalar { + boolean _any; + byte _widen; + // One of byte or int array + public final A _ary; + + TypeConAry( boolean any, byte widen, A ary ) { super(TCONARY); _any = any; _widen = widen; _ary = ary; } + public static void gather(ArrayList<Type> ts) { + ts.add(TypeConAryB.ABC); + ts.add(TypeConAryB.ABCD); + ts.add(TypeConAryI.I123); + } + // Fresh, uninterned type sharing this array; xdual must not intern. + abstract TypeConAry<A> _make(boolean any, byte widen); + + @Override TypeConAry<A> xdual() { return _make( !_any, (byte)(3-_widen) ); } + + @Override Type xmeet(Type t) { + TypeConAry ary = (TypeConAry)t; // Invariant + // Same base array but different? + if( _equals(ary) ) + // Return array with larger widen + return _widen >= ary._widen ? this : ary; + // Unrelated constant arrays, falls to some int range + return elem().meet(ary.elem()); + } + + Type ymeet( TypeInteger ti ) { + // if i can isa *each* element, then maybe can keep. + // no good to i.isa(elem()) because fails the dual + for( int j=0; j<len(); j++ ) + if( !ti.isa(TypeInteger.make(at8(j),at8(j),ti._widen)) ) + return ti.meet(elem()); + byte widen = (byte)Math.max(_widen,ti._widen); + return widen==_widen ? this : _make(_any,widen).intern(); + } + + private boolean _equals(TypeConAry ary) { + int len = len(); + if( len != ary.len() ) return false; + for( int i=0; i<len; i++ ) + if( at8(i) != ary.at8(i) ) + return false; + return true; + } + + @Override public boolean isHigh() { return this==TOP; } + @Override boolean _isConstant() { return true; } + @Override Type _makeStorage() { return this; } + + // Meet-over-elements type + public Type elem() { + if( _ary==null ) + return _any ? Type.TOP : BOTTOM; + int len = len(); + long min = Long.MAX_VALUE; + long max = Long.MIN_VALUE; + for( int i=0; i<len; i++ ) { + min = Math.min(min,at8(i)); + max = Math.max(max,at8(i)); + } + return TypeInteger.make(min,max,_widen); + } + public abstract long at8(int idx); + public abstract int len(); + @Override public abstract int log_size(); + + + public void write( BAOS baos ) { throw Utils.TODO("Should not reach here: abstract constant array cannot be written"); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java new file mode 100644 index 00000000..bddcb1fb --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.BAOS; +import java.util.Arrays; + +public class TypeConAryB extends TypeConAry<byte[]> { + private TypeConAryB(boolean any, byte widen, byte[] bs) { super(any,widen,bs); } + @Override TypeConAryB _make( boolean any, byte widen ) { return new TypeConAryB(any,widen,_ary); } + public static TypeConAryB make( byte[] bs ) { return new TypeConAryB(false,(byte)0,bs).intern(); } + public static TypeConAryB make( String s ) { return make(s.getBytes()); } + static final TypeConAryB ABC = make("abc"); + static final TypeConAryB ABCD = make("abcd"); + @Override public long at8(int idx) { return _ary[idx]; } + @Override public int len() { return _ary.length; } + @Override public int log_size() { return 0; } + @Override public String str() { return "[\""+new String(_ary)+"\"]"; } + @Override boolean eq(Type t) { return t instanceof TypeConAryB ary && _widen==ary._widen && Arrays.equals(_ary,ary._ary); } + @Override int hash() { return Arrays.hashCode(_ary); } + + @Override public void write( BAOS baos ) { baos.write(_ary); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java new file mode 100644 index 00000000..b1b061de --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.type; + +import java.util.Arrays; + +public class TypeConAryI extends TypeConAry<int[]> { + private TypeConAryI(boolean any, byte widen, int[] bs) { super(any,widen,bs); } + public static TypeConAryI make( int[] bs ) { return new TypeConAryI(false,(byte)0,bs).intern(); } + @Override TypeConAryI _make( boolean any, byte widen ) { return new TypeConAryI(any,widen,_ary); } + static final TypeConAryI I123 = make(new int[]{1,2,3}); + @Override public long at8(int idx) { return _ary[idx]; } + @Override public int len() { return _ary.length; } + @Override public int log_size() { return 2; } + @Override public String str() { return "[i32]"; } + @Override boolean eq(Type t) { return t instanceof TypeConAryI ary && _widen==ary._widen && Arrays.equals(_ary,ary._ary); } + @Override int hash() { return Arrays.hashCode(_ary); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFloat.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFloat.java new file mode 100644 index 00000000..8638b8c9 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFloat.java @@ -0,0 +1,111 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.AryInt; +import com.seaofnodes.simple.util.Utils; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Float Type + */ +public class TypeFloat extends TypeScalar { + + // - high -64, high -32, con 0, low +32, low +64 + public byte _sz; + + /** + * The constant value or 0 + */ + public double _con; + + private static final Ary<TypeFloat> FREE = new Ary<>(TypeFloat.class); + private TypeFloat(byte sz, double con) { super(TFLT); init(sz,con); } + private TypeFloat init( byte sz, double con ) { _sz = sz; _con = con; return this; } + public static TypeFloat malloc(byte sz, double con) { return FREE.isEmpty() ? new TypeFloat(sz,con) : FREE.pop().init(sz,con); } + private static TypeFloat make(int sz, double con) { + TypeFloat f = malloc((byte)sz,con); + TypeFloat t2 = f.intern(); + return t2==f ? f : t2.free(f); + } + @Override TypeFloat free(Type t) { + TypeFloat f = (TypeFloat)t; + f._hash = 0; + f._dual=null; + FREE.push(f); + return this; + } + + public static TypeFloat constant(double con) { return make(0, con); } + + 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); + + 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("Should not reach here: unknown float size"); + }; + } + private boolean isF32() { return ((float)_con)==_con; } + + @Override public boolean isHigh () { 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 + TypeFloat xdual() { + return _isConstant() ? this : new TypeFloat((byte)-_sz,0); // Constants are a self-dual + } + + @Override boolean _isConstant() { return _sz==0; } + + @Override Type _makeStorage() { + if( _isConstant() ) return isF32() ? F32 : F64; + return isHigh() ? dual() : this; + } + + @Override public Type makeZero() { return FZERO; } + + + @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/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java new file mode 100644 index 00000000..301fcc8b --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java @@ -0,0 +1,313 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.*; +import java.util.*; + +/** + * 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 Type[] _sig; + public Type _ret; + boolean _open; // Extra args are true:scalar BOT, false:scalar TOP + // Set of functions. Bit zero is used for mutable/immutable safety. Sign + // bit repeats infinitely. + public int[] _fidxs; + + private static final Ary<TypeFunPtr> FREE = new Ary<>(TypeFunPtr.class); + private TypeFunPtr(byte nil) { super(TFUNPTR,nil); } + private TypeFunPtr init(byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { + assert sig != null; + _nil = nil; + _open = open; + _sig = sig; + _ret = ret; + assert _fidxs == null; + _fidxs= XInt.intern(fidxs); + return this; + } + static TypeFunPtr malloc( byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { + return (FREE.isEmpty() ? new TypeFunPtr(nil) : FREE.pop()).init(nil,open,sig,ret,fidxs); + } + @Override TypeFunPtr free(Type t) { + TypeFunPtr fun = (TypeFunPtr)t; + assert !fun.isFree() && !fun._terned; + fun._sig = null; + fun._ret = null; + fun._dual = null; + fun._hash = 0; + fun._fidxs = null; + FREE.push(fun); + return this; + } + @Override boolean isFree() { return _sig==null; } + + // All fields directly listed + public static TypeFunPtr make( byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { + TypeFunPtr fun = malloc(nil,open,sig,ret,fidxs); + TypeFunPtr f2 = fun.intern(); + if( f2==fun ) return fun; + return VISIT.isEmpty() ? f2.free(fun) : f2.delayFree(fun); + } + // Trim trailing args which match the default. + private static TypeFunPtr makeAdjusted( byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { + Type def = open ? TypeScalar.BOT : TypeScalar.TOP; + int nargs = sig.length; + while( nargs > 0 && sig[nargs-1] == def ) + nargs--; + if( nargs != sig.length ) + sig = nargs == 0 ? ARG_EMPTY : Arrays.copyOf(sig,nargs); + return make(nil,open,sig,ret,fidxs); + } + public static TypeFunPtr make( boolean nil, Type[] sig, Type ret ) { return makeAdjusted((byte)(nil ? 3 : 2),true,sig,ret, XInt.FULL); } + public static TypeFunPtr make1( byte nil, boolean open, Type[] sig, Type ret, int fidx ) { + return makeAdjusted(nil,open,sig,ret,XInt.make(fidx)); + } + + + @Override TypeFunPtr makeFrom( byte nil ) { return nil ==_nil ? this : make( nil,_open,_sig,_ret, _fidxs); } + public TypeFunPtr makeFrom( Type ret ) { return ret ==_ret ? this : make( _nil,_open,_sig, ret, _fidxs); } + public TypeFunPtr makeFrom( int fidx ) { return make1(_nil,_open,_sig,_ret,fidx); } + public TypeFunPtr makeFrom( Type arg, int idx ) { + // Alter named arg + Type[] sig = _sig.clone(); + sig[idx] = arg; + return make(_nil,_open,sig,_ret,_fidxs); + } + @Override public Type nonZero() { return makeFrom((byte)(_nil <= 1 ? 1 : 2)); } + + public static final Type[] TEMPTY = new Type[0]; + static final Type[] TINT = new Type[]{TypeInteger.BOT}; + static final Type[] TINTMEM = new Type[]{TypeInteger.BOT,TypeFloat.F32}; + static final Type[] TINTINT = new Type[]{TypeInteger.BOT,TypeInteger.BOT}; + public static TypeFunPtr BOT = make((byte)3,true,TEMPTY,Type.BOTTOM, XInt.FULL); + public static TypeFunPtr TEST = make((byte)2,true,TINTMEM,TypeInteger.BOT, XInt.SET1); + public static TypeFunPtr TEST0 = make((byte)3,true,TINTMEM,TypeInteger.BOT, XInt.SET3); + public static TypeFunPtr MAIN = make((byte)3,true,TINT ,Type.BOTTOM, XInt.FULL); // Main can return anything + public static TypeFunPtr CALLOC= make((byte)3,true,TINTINT,TypeMemPtr .BOT, XInt.FULL); + public static void gather(ArrayList<Type> ts) { + ts.add(TEST); + ts.add(TEST0); + ts.add(BOT); + ts.add(MAIN); + ts.add(CALLOC); + ts.add(make((byte)2,false,TINT,TypeInteger.I32, XInt.SET1)); + ts.add(make((byte)2,true ,TINT,TypeInteger.BOT, XInt.SET3)); + ts.add(make1((byte)2,true, + new Type[]{TypeInteger.BOT}, + TypeInteger.BOT, 65)); + ts.add(make1((byte)2,true, + new Type[]{TypeInteger.BOT, TypeScalar.BOT}, + TypeInteger.BOT, 65)); + // Explicit trailing defaults must agree with the implicit argument tail. + for( boolean open : new boolean[]{false,true} ) { + Type def = open ? TypeScalar.BOT : TypeScalar.TOP; + ts.add(make1((byte)2,open,TINT,TypeInteger.I32,1)); + ts.add(make1((byte)2,open,new Type[]{TypeInteger.BOT,def,def},TypeInteger.I32,1)); + ts.add(make1((byte)2,open,new Type[]{def,def},TypeInteger.I32,1)); + ts.add(make1((byte)2,open,new Type[]{def,TypeInteger.BOT},TypeInteger.I32,1)); + } + } + + private static final Type[] ARG_EMPTY = new Type[0]; + @Override + Type xmeet(Type t) { + TypeFunPtr that = (TypeFunPtr) t; + boolean open = _open | that._open; + Type def0 = this._open ? TypeScalar.BOT : TypeScalar.TOP; + Type def1 = that._open ? TypeScalar.BOT : TypeScalar.TOP; + Type def = open ? TypeScalar.BOT : TypeScalar.TOP; + + int nargs = Math.max(_sig.length,that._sig.length); + Type[] args = new Type[nargs]; + + for( int i=0; i<nargs; i++ ) { + Type t0 = i < this._sig.length ? this._sig[i] : def0; + Type t1 = i < that._sig.length ? that._sig[i] : def1; + args[i] = t0.meet(t1); + } + + while( nargs>0 && args[nargs-1] == def ) + nargs--; + + if( nargs != args.length ) + args = nargs == 0 ? ARG_EMPTY : Arrays.copyOf(args,nargs); + + return make(xmeet0(that), open, args, _ret.meet(that._ret), XInt.meet(_fidxs,that._fidxs)); + } + + static Type[] xmeet( Type[] ts0, Type[] ts1) { + if( ts0==ts1 ) return ts0; + assert ts0.length==ts1.length; + Type[] ts = new Type[ts0.length]; + for( int i=0; i<ts0.length; i++ ) + ts[i] = ts0[i].meet(ts1[i]); + return ts; + } + + @Override TypeFunPtr xdual() { return malloc(dual0(), !_open, xdual(_sig), _ret.dual(), XInt.dual(_fidxs)); } + static Type[] xdual(Type[] ts) { + Type[] dual = new Type[ts.length]; + for( int i=0; i<ts.length; i++ ) + dual[i] = ts[i].dual(); + return dual; + } + + @Override TypeFunPtr rdual() { + if( _dual!=null ) return dual(); + assert !_terned; + Type[] sigs = new Type[_sig.length]; + TypeFunPtr d = malloc(dual0(),!_open,sigs,null, XInt.dual(_fidxs)); + (_dual = d)._dual = this; // Cross link duals + // Return rdual + d._ret = _ret._terned ? _ret.dual() : _ret.rdual(); + // Signature rdual + for( int i=0; i<_sig.length; i++ ) + sigs[i] = _sig[i]._terned ? _sig[i].dual() : _sig[i].rdual(); + return d; + } + + // RHS is NIL; deep-dual when crossing the centerline + @Override public Type meet0() { + if( _nil==3 ) return this; + if( _nil==2 ) return make((byte)3,_open,_sig,_ret,_fidxs); + + return make((byte)3,_open,_sig,_ret,_fidxs); + } + + @Override public boolean isHigh() { return _nil <= 1 || (_nil==2 && _fidxs== XInt.EMPTY); } + + @Override public boolean _isConstant() { return (_nil==2 && XInt.isConstant(_fidxs)) || (_nil==3 && _fidxs== XInt.EMPTY); } + + @Override boolean _isFinal() { return true; } + @Override TypeFunPtr _makeStorage() { return this; } + + @Override TypeFunPtr _close( String name, HashMap<String, Type> TYPES ) { + Type[] sig = new Type[_sig.length]; + TypeFunPtr fun = malloc(_nil,true,sig,null,_fidxs); + // Now start the recursion + fun._ret = _ret._close(name, TYPES ); + for( int i=0; i<sig.length; i++ ) + sig[i] = _sig[i]._close(name, TYPES ); + return fun; + } + + // Replace recursively all TypeBuilders with cyclic TypeStructs + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + Type[] sig = new Type[_sig.length]; + TypeFunPtr fun = malloc(_nil,_open,sig,null,_fidxs); + // Now start the recursion + fun._ret = _ret._upgradeType(TYPES); + for( int i=0; i<sig.length; i++ ) + sig[i] = _sig[i]._upgradeType(TYPES); + return fun; + } + + @Override public int log_size() { return 2; } // (1<<2)==4-byte pointers + + public Type arg(int i) { return _sig[i]; } + public int[] fidxs() { return _fidxs; } + public Type ret() { return _ret; } + public int nargs() { return _sig.length; } + public int fidx() { return XInt.onlyBit(_fidxs); } + public int nfcns() { + if( XInt.isHigh(_fidxs) ) return Integer.MAX_VALUE; // Infinite choices + return XInt.bitCount(_fidxs); + } + + + // Used to walk over type structures + @Override public int nkids() { return _sig.length+1; } + @Override public Type at( int idx ) { + return idx == _sig.length ? _ret : _sig[idx]; + } + @Override public void set( int idx, Type t ) { + if( idx == _sig.length ) _ret = t; + else _sig[idx] = t; + } + + + @Override + int hash() { + long hash = _ret.hashCode() ^ XInt.hash(_fidxs) ^ super.hash(); + for( Type arg : _sig ) + hash = hash*19 ^ arg.hashCode(); + return Utils.fold(hash); + } + + @Override + boolean eq(Type t) { + // Recursive; so use cyclic equals + if( !VISIT.isEmpty() ) { + assert TypeStruct.CEQUALS.isEmpty(); + boolean rez = cycle_eq(t); + TypeStruct.CEQUALS.clear(); + return rez; + } + TypeFunPtr fun = (TypeFunPtr)t; // Invariant + if( !super.eq(fun) || _open != fun._open || _fidxs != fun._fidxs || _sig.length != fun._sig.length ) return false; + if( _sig == fun._sig && _ret == fun._ret ) return true; + + if( !_ret.eq(fun._ret) ) return false; + for( int i = 0; i < _sig.length; i++ ) + if( !_sig[i].eq(fun._sig[i]) ) + return false; + return true; + } + @Override boolean cycle_eq(Type t) { + if( t._type != TFUNPTR ) return false; + TypeFunPtr fun = (TypeFunPtr)t; // Invariant + if( !super.eq(fun) || _open != fun._open || _fidxs != fun._fidxs || _sig.length != fun._sig.length ) return false; + if( _sig == fun._sig && _ret == fun._ret ) return true; + + // Check to see if we've ever compared this pair of types before; + // if so, then assume the cycle is equal here. + int pid = pid(fun); + if( TypeStruct.CEQUALS.find(pid)!= -1 ) return true; + TypeStruct.CEQUALS.push(pid); + if( !_ret.cycle_eq(fun._ret) ) return false; + for( int i = 0; i < _sig.length; i++ ) + if( !_sig[i].cycle_eq(fun._sig[i]) ) + return false; + return true; + } + + @Override public String str() { + if( this==BOT ) return "FunBot"; + if( this==BOT.dual() ) return "FunTop"; + return "{"+printFIDX()+"}"; + } + + @Override + SB _print(SB sb, BitSet visit, boolean html ) { + if( this==BOT ) return sb.p("FunBot"); + if( this==BOT.dual() ) return sb.p("FunTop"); + sb.p(x()).p("{ "); + if( _sig!=null ) + for( Type t : _sig ) + sb.p(t==null ? "---" : t.str()).p(" "); // Short form in signature + if( !_open ) sb.p("^ "); + _ret.print(sb.p(html ? "→ " : "-> "),visit,html).p(" #"); + // Print fidxs + return sb.p(printFIDX()).p("}").p(q()); + } + String printFIDX() { + String tilde = isHigh() ? "~" : ""; + String fidx = XInt.str(_fidxs); + return tilde+fidx; + } + + // Usage: for( long fidxs=fidxs(); fidxs!=0; fidxs=nextFIDX(fidxs) ) { int fidx = Long.numberOfTrailingZeros(fidxs); ... } + //public static long nextFIDX(long fidxs) { return fidxs & (fidxs-1); } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeInteger.java new file mode 100644 index 00000000..a2cf08b0 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeInteger.java @@ -0,0 +1,185 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.AryInt; +import com.seaofnodes.simple.util.Utils; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Integer Type + */ +public class TypeInteger extends TypeScalar { + /** + * 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 long _min, _max; + + public byte _widen; + + private static final Ary<TypeInteger> FREE = new Ary<>(TypeInteger.class); + private TypeInteger(long min, long max, byte widen) { super(TINT); init(min,max,widen); } + private TypeInteger init(long min, long max, byte widen) { + _min = min; _max = max; _widen=widen; + return this; + } + public static TypeInteger malloc(long lo, long hi, byte widen) { return FREE.isEmpty() ? new TypeInteger(lo,hi,widen) : FREE.pop().init(lo,hi,widen); } + public static TypeInteger make(long lo, long hi) { return make(lo,hi,(byte)0); } + public static TypeInteger make(long lo, long hi, byte widen) { + TypeInteger i = malloc(lo,hi,widen); + TypeInteger t2 = i.intern(); + return t2==i ? i : t2.free(i); + } + public TypeInteger makeWide(byte widen) { + if( widen == _widen ) return this; + if( _min == Long.MIN_VALUE && _max == Long.MAX_VALUE ) + return TypeInteger.BOT; + if( _min==_max ) return this; // Leave constants always narrow + return make(_min,_max,widen); + } + @Override TypeInteger free(Type t) { + TypeInteger i = (TypeInteger)t; + i._min = i._max = 0; + i._widen = 0; + i._hash = 0; + i._dual = null; + FREE.push(i); + return this; + } + + public Type same_but_slightly_wider_than( ) { + return _widen < 3 ? make(_min,_max, (byte)(_widen+1)) : BOT; + } + // Force max widen + @Override public Type widen() { + return _widen==3 ? this : make(_min,_max,(byte)3); + } + public static TypeInteger constant(long con) { return make(con, con); } + + 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 TWO = make(2,2); // Shows up in some Simple tests as the starting argument + + public final static TypeInteger I1 = make(-1,0,(byte)3); + public final static TypeInteger I8 = make(-128,127,(byte)3); + public final static TypeInteger I16 = make(-32768,32767,(byte)3); + public final static TypeInteger I32 = make(-1L<<31,(1L<<31)-1,(byte)3); + public final static TypeInteger BOT = make(Long.MIN_VALUE,Long.MAX_VALUE,(byte)3); + public final static TypeInteger TOP = (TypeInteger)BOT.dual(); + + public final static TypeInteger U1 = make(0,1,(byte)3); + public final static TypeInteger BOOL= U1; + public final static TypeInteger U8 = make(0,255,(byte)3); + public final static TypeInteger U16 = make(0,65535,(byte)3); + public final static TypeInteger U32 = make(0,(1L<<32)-1,(byte)3); + + public final static TypeInteger FATBOOL = make(0,1,(byte)2); + + public static void gather(ArrayList<Type> ts) { + ts.add(I32); ts.add(BOT); ts.add(U1); ts.add(I1); ts.add(U8); ts.add(ZERO); ts.add(FATBOOL); + // Ranges before and after excluding zero, at each widening level. + for( byte widen=0; widen<=3; widen++ ) { + ts.add(make(0,255,widen)); + ts.add(make(1,255,widen)); + ts.add(make(-255,0,widen)); + ts.add(make(-255,-1,widen)); + } + } + + @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 "i64"; + 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 int log_size() { + if( isHigh() ) return 0; // High types are dead, and should never hit code emission. + 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( _isConstant() ) return log(_min); // just const here + int lo = log(_min); + int hi = log(_max); + return Math.max(lo,hi); + } + + private static int log(long x) { + if( -0xFFL <= x && x <= 0xFFL) return 0; + if( -0xFFFFL <= x && x <= 0xFFFFL) return 1; + if(-0xFFFFFFFFL <= x && x <= 0xFFFFFFFFL) return 2; + return 3; + } + + 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), (byte)Math.max(_widen,i._widen)); + } + + @Override TypeInteger xdual() { return malloc(_max,_min,(byte)(3-_widen)); } + + @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),_widen); // specifically good on BOOL + if( _max==0 ) return make(_min,-1,_widen); + return this; + } + @Override public Type makeZero() { return ZERO; } + @Override boolean _isConstant() { return _min == _max; } + @Override Type _makeStorage() { + if( _isConstant() ) return BOT; + return isHigh() ? dual() : this; + } + + + @Override int hash() { return Utils.fold(_min) * Utils.fold(_max) + _widen; } + @Override public boolean eq( Type t ) { + return t instanceof TypeInteger i && _min==i._min && _max==i._max && _widen==i._widen; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMem.java new file mode 100644 index 00000000..4234a5e5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMem.java @@ -0,0 +1,229 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.*; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; + + +/** + * Represents a slice of memory corresponding to a set of aliases + */ +public class TypeMem extends Type { + + // Which slice of memory? + // 1 and TOP means no slice. + // 1 and BOT means all memory. + // N means alias slice#N. + public int _alias; + public Type _t; // Memory contents, some scalar type + public boolean _one; // Singleton memory, corresponding to TypeMemPtr._one. + public boolean _clz; // Class-object singleton memory; true singletons stay constant after construction. + public boolean _final;// Memory is only set in the constructor + + // Escaped aliases and functions - escaped into this memory. These are + // reachable from some chain of indirections from `_t`. Bit zero is + // used for mutable/immutable safety. Sign bit repeats infinitely. + public int[] _escFs; + public int[] _escAs; + + static public int CNT; + + private static final Ary<TypeMem> FREE = new Ary<>(TypeMem.class); + + private TypeMem() { super(TMEM); CNT++; } + private TypeMem init(int alias, Type t, boolean one, boolean clz, boolean xfinal, int[] fidxs, int[] aliases) { + _alias = alias; + _t = t; + _one = one; + _clz = clz; + // Public bulk memory is an aggregate of otherwise-unsplit aliases; + // a single final bit cannot describe it. Keep the lattice-dual high + // form distinct, and retain finality for private singleton memory and + // precise alias slices. + _final = t!=null && alias==1 && !one && !t.isHigh() ? false : xfinal; + + assert _escFs == null && _escAs == null; + _escFs = XInt.intern(fidxs); + _escAs = XInt.intern(aliases); + return this; + } + @Override TypeMem free(Type t) { + TypeMem mem = (TypeMem)t; + mem._alias= -99; + mem._dual = null; + mem._hash = 0; + mem._t = null; + mem._one = false; + mem._clz = false; + mem._final = false; + mem._escFs = null; + mem._escAs = null; + FREE.push(mem); + return this; + } + @Override boolean isFree() { return _alias == -99; } + + public static TypeMem malloc(int alias, Type t, boolean one, boolean clz, boolean xfinal, int[] fidxs, int[] aliases) { + return (FREE.isEmpty() ? new TypeMem() : FREE.pop()).init(alias,t,one,clz,xfinal,fidxs,aliases); + } + public static TypeMem make(int alias, Type t, boolean one, boolean clz, boolean xfinal, int[] fidxs, int[] aliases) { + if( fidxs ==null ) fidxs = XInt.EMPTY; + if( aliases==null ) aliases= XInt.EMPTY; + TypeMem f = malloc(alias,t,one,clz,xfinal, fidxs, aliases); + TypeMem f2 = f.intern(); + if( f2==f ) return f; + return VISIT.isEmpty() ? f2.free(f) : f2.delayFree(f); + } + public static TypeMem make(int alias, Type t) { return make(alias,t,false,false,false,null,null); } + // Make a private memory + public static TypeMem makePrivate(Type t) { return make(1,t,true,false,false,escapeFIDX(XInt.EMPTY,t), escapeAlias(XInt.EMPTY,t)); } + + public TypeMem makeFrom(int alias) { return make(alias,_t,_one,_clz,_final,_escFs,_escAs); } + public TypeMem makeFrom(int[] fidxes, int[] aliases) { + return make(_alias,_t,_one,_clz,_final,fidxes,aliases); + } + // Existing escapes plus `t` escapes + public TypeMem escapes(Type t) { + return make(_alias,_t,_one,_clz,_final, escapeFIDX(_escFs,t), escapeAlias(_escAs,t)); + } + // Existing escapes plus `t` escapes + public TypeMem escapesFrom(Type t) { + return make(_alias,_t,_one,_clz,_final, escapeFIDX(_escFs,t), escapeAlias(_escAs,t)); + } + public TypeMem escapesAliases(Type t) { + return make(_alias,_t,_one,_clz,_final, _escFs, escapeAlias(_escAs,t)); + } + + public static final TypeMem TOP = make(1, Type.TOP ,true ,true, true , XInt.EMPTY, XInt.EMPTY); + public static final TypeMem BOT = make(1, Type.BOTTOM,false,false,true , XInt.FULL , XInt.FULL ); + public static final TypeMem START= make(1,Type.BOTTOM,false,false,false, XInt.EMPTY, XInt.EMPTY); + public static final TypeMem SELF_MEM = make(1, TypeStruct.BOT); + + public static void gather(ArrayList<Type> ts) { + ts.add(make(2,Type.NIL)); + ts.add(make(2,TypeInteger.ZERO)); + ts.add(BOT); + ts.add(SELF_MEM); + ts.add(START); + ts.add(make(2,TypeInteger.ZERO,true,false,true,XInt.EMPTY,XInt.make(2))); + ts.add(make(2,TypeInteger.BOT,false,false,false,XInt.EMPTY,XInt.FULL)); + } + + @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; + // Treat a alias-1 has either very-high or very-low, according to "_t" + int alias = _alias==that._alias ? _alias // Same-same + : this._alias==1 && this._t.isHigh() ? that._alias // this is high-1 alias, so take that + : that._alias==1 && that._t.isHigh() ? this._alias // that is high-1 alias, so take this + : 1; + Type mt = _t.meet(that._t); + int[] fidxs = XInt.meet( _escFs, that._escFs ); + int[] aliases= XInt.meet( _escAs, that._escAs ); + // Singleton and class facts are kept only when both sides agree. + // Finality is a low-side fact, and flips on the dual side. + boolean highAlias1 = (this._alias==1 && this._t.isHigh()) || (that._alias==1 && that._t.isHigh()); + boolean xfinal = highAlias1 ? (_final & that._final) : (_final | that._final); + return make(alias, mt, _one & that._one, _clz & that._clz, xfinal, fidxs, aliases); + } + + @Override + Type xdual() { return malloc(_alias,_t.dual(),!_one,!_clz,!_final, XInt.dual(_escFs), XInt.dual(_escAs)); } + + @Override TypeMem rdual() { + if( _dual!=null ) return dual(); + assert !_terned; + TypeMem d = malloc(_alias,null,!_one,!_clz,!_final, XInt.dual(_escFs), XInt.dual(_escAs)); + (_dual = d)._dual = this; // Cross link duals + d._t = _t._terned ? _t.dual() : _t.rdual(); + if( d._alias==1 && !d._one ) { assert !d._t.isHigh(); d._final = false; } + return d; + } + + @Override public TypeMem makeZero() { return TOP; } + @Override public boolean isHigh() { return _t.isHigh(); } + @Override boolean _isConstant() { return _one && _clz && _alias!= 1 && _t._isConstant(); } + @Override public int log_size() { throw Utils.TODO("Should not reach here: memory has no scalar storage size"); } + @Override boolean _isFinal() { throw Utils.TODO("Should not reach here"); } + @Override TypeMem _makeStorage() { throw Utils.TODO("Should not reach here"); /*return make(_alias,_t._makeStorage(),_one,_clz,_final,_escFs,_escAs);*/ } + + @Override TypeMem _close( String name, HashMap<String, Type> TYPES ) { return malloc(_alias,_t._close(name, TYPES ),_one,_clz,_final,_escFs,_escAs); } + + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + return make(_alias,_t._upgradeType(TYPES),_one,_clz,_final,_escFs,_escAs); + } + + @Override int hash() { return 9876543 + _alias + _t.hashCode() + (_one ? 8196 : 0) + (_clz ? 4098 : 0) + (_final ? 16392 : 0) + XInt.hash(_escFs) + XInt.hash(_escAs); } + + @Override boolean eq(Type t) { + TypeMem that = (TypeMem) t; // Invariant + return _alias == that._alias && _t == that._t && _one == that._one && _clz == that._clz && _final == that._final && _escFs == that._escFs && _escAs == that._escAs; + } + + @Override boolean cycle_eq(Type t) { + TypeMem that = (TypeMem) t; // Invariant + return _alias == that._alias && _one == that._one && _clz == that._clz && _final == that._final && _escFs == that._escFs && _escAs == that._escAs && _t.cycle_eq(that._t); + } + + @Override public int nkids() { return 1; } + @Override public Type at( int idx ) { return _t; } + @Override public void set( int idx, Type t ) { _t = t; } + + + @Override public SB _print(SB sb, BitSet visit, boolean html) { + if( this==BOT ) return sb.p("MemBot"); + if( this==TOP ) return sb.p("MemTop"); + sb.p('#'); + if( !_final ) sb.p('!'); // Mutable memory + if( _one ) sb.p(_clz ? 'z' : '-'); + if( _alias==0 ) return sb.p(_t._type==TTOP ? "TOP" : "BOT"); + sb.p(_alias).p(":["); + _t.print(sb,visit,html).p(","); + XInt.str(sb,_escFs).p(","); + return XInt.str(sb,_escAs).p("]"); + } + + @Override public String str() { + if( this==BOT ) return "MemBot"; + if( this==TOP ) return "MemTop"; + SB sb = new SB().p('#'); + if( !_final ) sb.p('!'); // Mutable memory + if( _one ) sb.p(_clz ? 'z' : '-'); + if( _alias==0 ) return sb.p(_t._type==TTOP ? "TOP" : "BOT").toString(); + sb.p(_alias).p(":[").p(_t.str()).p(","); + XInt.str(sb,_escFs).p(","); + return XInt.str(sb,_escAs).p("]").toString(); + } + + + // Tracking bulk escaped aliases and fidxs + + // Escape all FIDXs from t + private static int[] escapeFIDX( int[] xs, Type t ) { + return switch(t) { + case TypeMemPtr tmp -> escapeFIDX(xs,tmp._obj); + case TypeStruct ts -> ts._open && !ts.isAry() + ? XInt.FULL // Open structs can have any fields + : XInt.meet(xs,ts.fidxs()); + case TypeFunPtr tfp -> XInt.meet(xs,tfp.fidxs()); + case TypeMem mem -> XInt.meet(xs,mem._escFs); + case Type tt -> tt == BOTTOM ? XInt.FULL : xs; + }; + } + + private static int[] escapeAlias( int[] xs, Type t ) { + return switch(t) { + case TypeMemPtr tmp -> escapeAlias(xs,tmp._obj); + case TypeStruct ts -> ts._open && !ts.isAry() + ? XInt.FULL // Open structs can have any fields + : XInt.meet(xs,ts.aliases()); + case TypeMem mem -> XInt.meet(xs,mem._escAs); + case Type tt -> tt == BOTTOM ? XInt.FULL : xs; + }; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java new file mode 100644 index 00000000..73ca6190 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java @@ -0,0 +1,179 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.AryInt; +import com.seaofnodes.simple.util.SB; +import java.util.*; + +/** + * 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 TypeStruct _obj; + // Deep read-only access through this pointer; independent of slot finality. + public boolean _ro; + + public boolean _one; // Singleton pointer value; the pointer itself is constant. + public boolean _pub; // Escaped/public pointer. False means constructor-private. + + private static final Ary<TypeMemPtr> FREE = new Ary<>(TypeMemPtr.class); + private TypeMemPtr(byte nil, TypeStruct obj, boolean one, boolean pub) { super(TMEMPTR,nil); init(nil,obj,one,pub); } + private TypeMemPtr init(byte nil, TypeStruct obj, boolean one, boolean pub) { _nil=nil; _obj=obj; _ro=obj==TypeStruct.BOT; _one=one; _pub=pub; return this; } + // Return a filled-in TypeMemPtr; either from free list or alloc new. + static TypeMemPtr malloc(byte nil, TypeStruct obj, boolean one, boolean pub) { + return FREE.isEmpty() ? new TypeMemPtr(nil,obj,one,pub) : FREE.pop().init(nil,obj,one,pub); + } + + // All fields directly listed + public static TypeMemPtr make(byte nil, TypeStruct obj, boolean one) { + return make(nil,obj,one,true); + } + public static TypeMemPtr make(byte nil, TypeStruct obj, boolean one, boolean pub) { + return make(nil,obj,one, pub,obj==TypeStruct.BOT); + } + private static TypeMemPtr make(byte nil, TypeStruct obj, boolean one, boolean pub, boolean ro) { + TypeMemPtr tmp = malloc(nil, obj, one, pub).access(ro); + TypeMemPtr t2 = tmp.intern(); + if( t2==tmp ) return tmp; + return VISIT.isEmpty() ? t2.free(tmp) : t2.delayFree(tmp); + } + @Override TypeMemPtr free(Type t) { + TypeMemPtr tmp = (TypeMemPtr)t; + tmp._nil = -99; + tmp._obj = null; + tmp._one = false; + tmp._pub = true; + tmp._dual = null; + tmp._hash = 0; + FREE.push(tmp); + return this; + } + @Override boolean isFree() { return _obj==null; } + + static TypeMemPtr make(byte nil, TypeStruct obj) { return make(nil, obj, false); } + public static TypeMemPtr make (TypeStruct obj) { return make((byte)2, obj); } + public static TypeMemPtr makeNullable(TypeStruct obj) { return make((byte)3, obj); } + public static TypeMemPtr makePrivate (TypeStruct obj) { return make((byte)2, obj, false, false); } + + public TypeMemPtr makeFrom(TypeStruct obj) { return obj==_obj ? this : make(_nil, obj, _one, _pub,_ro); } + public TypeMemPtr makeNullable() { return makeFrom((byte)3); } + /** Inferred mutable local: nullable, public and non-singleton. */ + public TypeMemPtr makeVar() { return (TypeMemPtr)makeStorage(); } + /** Public-memory shape: nullable and non-singleton, preserving the referent. */ + @Override TypeMemPtr _makeStorage() { return make(_nil,_obj._makeStorage(),false,true,_ro); } + @Override TypeMemPtr makeFrom(byte nil) { return nil==_nil ? this : make(nil, _obj, _one, _pub,_ro); } + @Override public Type nonZero() { return makeFrom((byte)(_nil <= 1 ? 1 : 2)); } + + private TypeMemPtr access(boolean ro) { _ro=ro; return this; } + + public TypeMemPtr withAccess(boolean ro) { + if( _ro==ro ) return this; + TypeMemPtr ptr = malloc(_nil, _obj, _one, _pub); + ptr._ro = ro; + TypeMemPtr res = ptr.intern(); + if( res==ptr ) return ptr; + return VISIT.isEmpty() ? res.free(ptr) : res.delayFree(ptr); + } + + // Writable slots can load and store references, so their element access + // must agree in both directions. A read-only outer view can weaken access. + @Override public boolean accessISA(Type dst) { + if( !(dst instanceof TypeMemPtr ptr) ) return true; + if( _ro && !ptr._ro ) return false; + if( ptr._ro || _obj._fields==null || ptr._obj._fields==null || !_obj.isAry() || !ptr._obj.isAry() ) return true; + Type a = _obj._fields[1]._t, b = ptr._obj._fields[1]._t; + return a.accessISA(b) && b.accessISA(a); + } + + // 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 final TypeMemPtr BOT = make((byte)3, TypeStruct.BOT); + public static final TypeMemPtr TOP = BOT.dual(); + public static final TypeMemPtr NOTBOT = make((byte)2,TypeStruct.BOT); + + public static final TypeMemPtr TEST= make((byte)2, TypeStruct.TEST); + public static void gather(ArrayList<Type> ts) { ts.add(NOTBOT); ts.add(BOT); ts.add(TEST); ts.add(TEST.makeRO()); } + + @Override + public TypeNil xmeet(Type t) { + TypeMemPtr that = (TypeMemPtr) t; + assert !isFree() && !that.isFree(); + // Can't keep _one if mixing two unequal singletons; the result is not + // either singleton. Once either side is public, the meet is public. + boolean one = _one && that._one && (_obj==that._obj || !(_obj.isConAry() || that._obj.isConAry())); + return make(xmeet0(that), (TypeStruct)_obj.meet(that._obj), one, _pub | that._pub,_ro | that._ro); + } + + @Override + TypeMemPtr xdual() { return malloc( dual0(), _obj.dual(), !_one, !_pub).access(!_ro); } + + @Override TypeMemPtr rdual() { + if( _dual!=null ) return dual(); + assert !_terned; + TypeMemPtr d = malloc(dual0(), null, !_one, !_pub).access(!_ro); + (_dual = d)._dual = this; // Cross link duals + d._obj = _obj._terned ? _obj.dual() : _obj.rdual(); + return d; + } + + // RHS is NIL; do not deep-dual when crossing the center line + @Override Type meet0() { return _nil==3 ? this : make((byte)3,_obj,false,true,_ro); } + + @Override boolean _isConstant() { return _one; } + @Override boolean _isFinal() { return _ro; } + @Override TypeMemPtr _makeRO() { return withAccess(true); } + @Override TypeMemPtr _close( String name, HashMap<String, Type> TYPES ) { return malloc(_nil,_obj._close(name, TYPES ),_one,_pub).access(_ro); } + + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + return makeFrom((TypeStruct)_obj._upgradeType(TYPES)); + } + + @Override public int log_size() { return 3; } // (1<<3)==8-byte pointers + + @Override int hash() { return (_ro ? 8192 : 0) ^ _obj.hashCode() ^ super.hash() ^ (_one ? 2048 : 0) ^ (_pub ? 4096 : 0); } + + @Override boolean eq(Type t) { + TypeMemPtr ptr = (TypeMemPtr)t; // Invariant + return _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _pub == ptr._pub && _obj == ptr._obj; + } + @Override boolean cycle_eq(Type t) { + if( t._type != TMEMPTR ) return false; + TypeMemPtr ptr = (TypeMemPtr)t; // Invariant + return _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _pub == ptr._pub && _obj.cycle_eq(ptr._obj); + } + + @Override public int nkids() { return 1; } + @Override public Type at( int idx ) { return _obj; } + @Override public void set( int idx, Type t ) { _obj = (TypeStruct)t; } + + + @Override public String str() { + if( this==BOT ) return "PtrBot"; + if( this==TOP ) return "PtrTop"; + if( this== NOTBOT) return "*void"; + return x()+"*"+(_obj==null?"---":_obj.str())+q(); + } + + @Override SB _print(SB sb, BitSet visit, boolean html ) { + if( this==BOT ) return sb.p("PtrBot"); + if( this==TOP ) return sb.p("PtrTop"); + if( this== NOTBOT) return sb.p("*void"); + return _obj.print(sb.p(x()).p("*"),visit,html).p(q()); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeNil.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeNil.java new file mode 100644 index 00000000..2c1c1d76 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeNil.java @@ -0,0 +1,61 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.ArrayList; + +/** + * Nil-able Scalar types + */ +public abstract class TypeNil extends TypeScalar { + // 0 = high-subclass choice nil + // 1 = high-subclass no nil + // 2 = low -subclass no nil + // 3 = low -subclass also nil + 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 isHighOrConst() { return isHigh() || isConstant(); } + + public boolean notNull() { return _nil==1 || _nil==2; } + public boolean nullable() { return _nil==3; } + + @Override boolean _isConstant() { return false; } + + final String q() { return _nil == 1 || _nil == 2 ? "" : "?"; } + final String x() { return isHigh() ? "~" : ""; } + + int hash() { return _nil<<17; } + + @Override boolean eq(Type ptr) { return ptr instanceof TypeNil nil && _nil ==nil._nil; } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypePtr.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypePtr.java new file mode 100644 index 00000000..7e9e70e4 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypePtr.java @@ -0,0 +1,57 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.AryInt; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; + +/** + * Represents a Scalar; a single register-sized value. + */ +public class TypePtr extends TypeNil { + private static final String[] STRS = new String[]{"~ptr","~nptr","nptr","ptr"}; + + 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 = (TypePtr)XNPTR.dual(); + public static TypePtr PTR = (TypePtr)XPTR.dual(); + 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("Should not reach here: plain pointer nil state is canonical"); } + + @Override public TypeNil xmeet(Type t) { + TypePtr that = (TypePtr) t; + return PTRS[xmeet0(that)]; + } + + @Override TypePtr xdual() { return new TypePtr((byte)(3-_nil)); } + + // 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 TypePtr _makeStorage() { return PTR; } + + + @Override public String str() { return STRS[_nil]; } + @Override public SB _print(SB sb, BitSet visit, boolean html) { return sb.p(str()); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeRPC.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeRPC.java new file mode 100644 index 00000000..7acc99de --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeRPC.java @@ -0,0 +1,86 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.Ary; +import java.util.*; + +/** + * Return Program Control or Return PC or RPC + */ +public class TypeRPC extends Type { + + // A set of CallEndNode IDs (or StopNode); commonly just one. High-bit + // XInt sets represent complements/infinite sets. + public int[] _rpcs; + + private static final Ary<TypeRPC> FREE = new Ary<>(TypeRPC.class); + private TypeRPC init(int[] rpcs) { _rpcs = XInt.intern(rpcs); return this; } + private TypeRPC() { super(TRPC); } + private static TypeRPC malloc(int[] rpcs) { + return (FREE.isEmpty() ? new TypeRPC() : FREE.pop()).init(rpcs); + } + private static TypeRPC make(int[] rpcs) { + TypeRPC rpc = malloc(rpcs); + TypeRPC t2 = rpc.intern(); + return t2==rpc ? rpc : t2.free(rpc); + } + @Override TypeRPC free(Type t) { + TypeRPC rpc = (TypeRPC)t; + rpc._rpcs = null; + rpc._dual = null; + rpc._hash = 0; + FREE.push(rpc); + return this; + } + @Override boolean isFree() { return _rpcs==null; } + + public static TypeRPC constant(int cend) { + return make(XInt.make(cend)); + } + + public int rpc() { + return XInt.onlyBit(_rpcs); + } + + + public static final TypeRPC BOT = make(XInt.FULL); + private static final TypeRPC TEST2 = constant(2); + private static final TypeRPC TEST3 = constant(2); + public static final TypeRPC EMPTY = make(XInt.EMPTY); + + public static void gather(ArrayList<Type> ts) { ts.add(BOT); ts.add(TEST2); ts.add(TEST3); } + + @Override public TypeRPC makeZero() { return EMPTY; } + + + @Override public String str() { + if( _rpcs == XInt.FULL ) return "$[ALL]"; + if( _rpcs == XInt.EMPTY ) return "$[]"; + return "$"+XInt.str(_rpcs); + } + + @Override + public TypeRPC xmeet(Type other) { + TypeRPC rpc = (TypeRPC)other; + return make(XInt.meet(_rpcs,rpc._rpcs)); + } + + @Override TypeRPC xdual() { return malloc(XInt.dual(_rpcs)); } + @Override Type rdual() { + TypeRPC d = xdual(); + (_dual = d)._dual = this; + return d; + } + + @Override boolean _isConstant() { return XInt.isConstant(_rpcs); } + @Override TypeRPC _makeStorage() { return this; } + + + @Override + int hash() { return XInt.hash(_rpcs); } + @Override + public boolean eq( Type t ) { + TypeRPC rpc = (TypeRPC)t; // Contract + return _rpcs==rpc._rpcs; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeScalar.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeScalar.java new file mode 100644 index 00000000..e369e6a5 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeScalar.java @@ -0,0 +1,45 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.SB; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; + +/** + * The envelope for values which can live in an ordinary register or memory + * field: integers, floats, memory pointers, and function pointers. + * + * `BOT` means any scalar value and `TOP` means no scalar value. They sit + * strictly inside the global {@link Type#BOTTOM}/{@link Type#TOP} pair, which + * also includes non-values such as control and memory. + */ +public class TypeScalar extends Type { + protected TypeScalar(byte type) { super(type); } + private TypeScalar() { this(TSCALAR); } + + public static final TypeScalar BOT = new TypeScalar().intern(); + public static final TypeScalar TOP = BOT.dual(); + + public static void gather(ArrayList<Type> ts) { ts.add(BOT); } + + /** Meet two different scalar families. */ + final Type smeet(TypeScalar scalar) { + if( _type == TSCALAR ) return isHigh() ? scalar : this; + if( scalar._type == TSCALAR ) return scalar.isHigh() ? this : scalar; + return BOT; + } + + @Override Type xmeet(Type t) { + TypeScalar scalar = (TypeScalar)t; + return isHigh() ? scalar : this; + } + + @Override TypeScalar xdual() { return new TypeScalar(); } + @Override public boolean isHigh() { return this == TOP; } + @Override Type _makeStorage() { return BOT; } + + + @Override public String str() { return isHigh() ? "~scalar" : "scalar"; } + @Override SB _print(SB sb, BitSet visit, boolean html) { return sb.p(str()); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeStruct.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeStruct.java new file mode 100644 index 00000000..67169009 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeStruct.java @@ -0,0 +1,640 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.util.*; +import java.util.*; + +/** + * 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 is essentially another field, + // immutable and type ignored (BOT). + + /** + * `_open` and `_fref` are independent lattice coordinates. + * + * `_open` says the structural tail is not known yet. On the low side an + * open struct has infinitely many implicit BOT fields; its dual has an + * implicit TOP tail. A real declaration is therefore open while its body + * is being parsed: + * + * ```text + * struct S { int x; /* more declared fields may follow *\/ } + * ``` + * + * `_fref` says no actual declaration has contributed to the type. The + * parser may still invent fields demanded by uses, leaving a useful open + * structural constraint: + * + * ```text + * S? p; p.x; // fref S { BOT x; ... } + * ``` + * + * Meeting that constraint with a real definition ANDs `_fref` off while + * retaining and checking the invented field. Closing recursive types must + * not by itself clear `_fref`; closure discovers shape, not a declaration. + */ + + public String _name; // Struct name + public boolean _open; + public boolean _fref; + public Field[] _fields; + + private TypeStruct() { super(TSTRUCT); } + private static final Ary<TypeStruct> FREE = new Ary<>(TypeStruct.class); + private TypeStruct init(String name, boolean open, boolean fref, Field[] fields) { + // Check for const array element name with mutable field + //assert !name.startsWith("[~]") || fields.length!=2 || fields[1]._t==null || (fields[1]._final != fields[1]._t.isHigh()); + _name = name; + _open = open; + _fref = fref; + _fields = fields; + return this; + } + // Return a filled-in TypeStruct; either from free list or alloc new. + private static TypeStruct malloc(String name, boolean open, boolean fref, Field[] fields) { + return (FREE.isEmpty() ? new TypeStruct() : FREE.pop()).init(name,open,fref,fields); + } + // Free ts; return this. + @Override TypeStruct free(Type t) { + TypeStruct ts = (TypeStruct)t; + assert !ts.isFree() && !ts._terned; + ts._name = null; + ts._fields = null; + ts._offs = null; + ts._dual = null; + ts._hash = 0; + ts._aliases = null; + ts._fidxs = null; + FREE.push(ts); + return this; + } + @Override boolean isFree() { return _fields==null; } + + + // All fields directly listed + public static TypeStruct make(String name, boolean open, Field... fields) { + return make(name,open,false,fields); + } + public static TypeStruct make(String name, boolean open, boolean fref, Field... fields) { + TypeStruct ts = malloc(name, open, fref, fields); + TypeStruct t2 = ts.intern(); + if( t2==ts ) return ts; + return VISIT.isEmpty() ? t2.free(ts) : ts.delayFree(ts); + } + + // Summary aliases, after interning. Skips private + // fields which need to be explicitly loaded. + private int[] _aliases; + public int[] aliases() { + if( _aliases != null ) return _aliases; + if( _fields.length==0 ) return (_aliases=XInt.EMPTY); + assert _terned; + int maxAlias = 0; + for( Field f : _fields ) + maxAlias = Math.max(maxAlias,f._alias); + int len = XInt.idx(maxAlias)+1; + if( (maxAlias&31)==31 ) len++; + int[] xs = XInt.free(len); + for( Field f : _fields ) + if( f._fname.charAt(0) != '_' && // Skip private fields. + !hiddenInit(f) && // Skip private allocation helper. + f._t != Type.TOP ) // Skip uninit fields + xs[XInt.idx(f._alias)] |= XInt.mask(f._alias); + return (_aliases=XInt.intern(xs)); + } + + // Summary fidxs, after interning. Skips private + // fields which need to be explicitly loaded. + private int[] _fidxs; + public int[] fidxs() { + if( _fidxs != null ) return _fidxs; + assert _terned; + int[] xs = XInt.EMPTY; + for( Field f : _fields ) + if( f._fname.charAt(0) != '_' && // Skip private fields. + !hiddenInit(f) && // Skip private allocation helper. + f._t instanceof TypeFunPtr tfp ) // Looking for TFP + xs = XInt.meet(xs,tfp._fidxs); + return (_fidxs = xs); + } + + // With a user constructor, the same-named function field is the hidden + // default <init> allocation helper, not part of the public class surface. + public boolean hiddenInit(Field fld) { + if( field("<ctor>") == null || !_name.startsWith("class:") ) return false; + int idx = _name.lastIndexOf('.'); + String name = _name.substring(idx<0 ? "class:".length() : idx+1); + return fld._fname.equals(name); + } + + // New open struct with no fields + public static TypeStruct open( String name ) { return make(name,true); } + public static TypeStruct forward( String name ) { return make(name,true,true); } + public TypeStruct defined() { return _fref ? make(_name,_open,false,_fields) : this; } + + // Array, variant body + public static TypeStruct makeAry(String name, TypeInteger len, int lenAlias, Type body, int bodyAlias, boolean efinal) { + return make(name,false, + Field.make("#" , len, lenAlias, true ), + Field.make("[]",body,bodyAlias,efinal)); + } + // Array, known constant body + public static TypeStruct makeAry(String name, int lenAlias, int bodyAlias, TypeConAry con) { + Type len = TypeInteger.constant(con.len()); + return make(name,false, + Field.make("#" ,len , lenAlias,true), + Field.make("[]" ,con.elem(),bodyAlias,true), + Field.make("[con]",con ,bodyAlias,true)); + } + + public TypeStruct makeInit() { + Field[] fs = new Field[_fields.length]; + for( int i=0; i<_fields.length; i++ ) { + if( _fields[i]._extern ) { fs[i] = _fields[i]; continue; } + Type zero = _fields[i]._t.makeZero(); + fs[i] = _fields[i].makeFrom(_fields[i]._t != Type.BOTTOM && zero.isa(_fields[i]._t) ? zero : Type.TOP); + } + return make(_name,_open,_fref,fs); + } + + public TypeStruct add( Field f ) { + assert find(f._fname)==-1; // No double field names + Field[] flds = Arrays.copyOf(_fields,_fields.length+1); + flds[_fields.length] = f; + return make(_name,_open,_fref,flds); + } + public TypeStruct replace( Field f ) { + Field[] flds = Arrays.copyOf(_fields,_fields.length); + flds[find(f._fname)] = f; + return make(_name,_open,_fref,flds); + } + public TypeStruct remove( int i ) { + Field[] flds = new Field[_fields.length-1]; + System.arraycopy(_fields, 0,flds,0,i); + System.arraycopy(_fields,i+1,flds,i,flds.length-i); + return make(_name,_open,_fref,flds); + } + + public TypeStruct addOrUpdate(Field fld) { + int idx = find(fld._fname); + int len = idx == -1 ? (idx=_fields.length)+1 : _fields.length; + Field[] flds = Arrays.copyOf(_fields,len); + flds[idx] = fld; + return make(_name,_open,_fref,flds); + } + + public final TypeStruct close() { + return (TypeStruct)recurOpen()._close(_name, null).recurClose(); + } + @Override TypeStruct _close( String name, HashMap<String, Type> TYPES ) { + assert (name==null) != (TYPES==null); // One or the other + String namePlusFinal = isAry() ? (_name+(_fields[1]._final ? "~" : "!")).intern() : _name; + TypeStruct ts = (TypeStruct)VISIT.get(namePlusFinal); + if( ts!=null ) return ts; + TypeStruct base = name==null ? (TypeStruct)TYPES.get(_name) : this; + assert base != null; + boolean open = base._fref ? base._open : name != _name && name != null && _open; + ts = base.recurPre(namePlusFinal, _name, open, base._fref ); + Field[] flds = ts._fields; + // Now start the recursion + for( int i=0; i<flds.length; i++ ) + flds[i].setType(base._fields[i]._t._close(name, TYPES)); + + // Note: this whole complex shenanigans here is because there are + // actually two flavors of any array (non-/final contents) but I + // allow the parser to represent both with the same name string. + if( isAry() && _fields[1]._final && !flds[1]._final ) // This is a final array variant + flds[1] = flds[1].makeFrom(true); // Add final to the base TYPES non-final array + + return ts; + } + + // Needs full recursive treatment; e.g. + // struct LL { LL? next; int !q; } // Q is mutable field. + // Stored into a not-mutable, so need that version also: + // struct LL { LL? next; int q; } // Q is NOT mutable field. + // Now upgrade: + // it is not the default version from TYPES. + // it is fully cyclic + // + // Evil question: has no *open* internal fields, so can I assume it is fully correct and never needs upgrade? + // + // upgrades are for well-formed types with internal ptrs to early open + // versions of other types, as happens in all the CoRecur tests. + // During the co-recur tests, can I make non-default cycles with + // weaker versions of other early types? + // Seems plausible... + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + TypeStruct base = (TypeStruct)TYPES.get(_name); + if( this == base ) return this; // Already the good form + TypeStruct ts = (TypeStruct)VISIT.get(_uid); + if( ts!=null ) return ts; + // Main work: replace open with closed + if( _open ) + return base; + // Build recursive version + ts = recurPre(_uid,_name,false,_fref); + Field[] flds = ts._fields; + // Now start the recursion + for( int i=0; i<flds.length; i++ ) + flds[i].setType(_fields[i]._t._upgradeType(TYPES)); + return ts; + } + + static final AryInt CEQUALS = new AryInt(); + + public static final String TOPNAME = "$TOP", BOTNAME = "$BOT"; + public static final TypeStruct TOP = make("$TOP",false); + public static final TypeStruct BOT = TOP.dual(); + public static final TypeStruct TEST= make("test",false,Field.TEST); + private static final TypeStruct ARY = makeAry("[]i64",TypeInteger.U32,-1,TypeInteger.BOT,-2,false); + private static final TypeStruct STR = makeAry("[]u8" ,TypeInteger.U32,-1,TypeInteger.U8 ,-4,false); + private static final TypeStruct ABC = makeAry("[]u8",-1,-4,TypeConAryB.ABC); + // Representative unresolved forms for the exhaustive lattice tests: + // bare forward ref, forward ref constrained by a field use, and definition. + private static final TypeStruct FREF0 = forward("%FREF"); + private static final TypeStruct FREF1 = FREF0.add(Field.make("x",Type.BOTTOM,-20,false)); + private static final TypeStruct FDEF1 = make("%FREF",false,Field.make("x",TypeInteger.BOT,-20,false)); + + // A pair of self-cyclic types + private static final TypeStruct SINT0 = make("%SINT",false); + private static final TypeStruct SFLT0 = make("%SFLT",false); + public static final TypeStruct SINT1 = SINT0.add(Field.make("a", TypeInteger.U32, -1, false)).add(Field.make("s2",TypeMemPtr.make((byte)2,SFLT0),-2, false)); + public static final TypeStruct SFLT1 = SFLT0.add(Field.make("b", TypeFloat .F32, -3, false)).add(Field.make("s1",TypeMemPtr.make((byte)2,SINT1),-4, false)); + + @Override public boolean isHigh() { return _name==TOPNAME; } + + public static void gather(ArrayList<Type> ts) { + ts.add(BOT); + ts.add(TEST); + ts.add(ARY); + ts.add(STR); + ts.add(ABC); + ts.add(FREF0); + ts.add(FREF1); + ts.add(FDEF1); + ts.add(SINT0); + ts.add(SFLT0); + ts.add(SINT1); + ts.add(SFLT1); + // Break cyclic init: built a struct + Field fcalloc = Field.make("calloc",TypeFunPtr.CALLOC,-2,true); + TypeStruct scalloc = make("calloc",false,fcalloc); + ts.add(scalloc); + + } + + // Find field index by name + public int find(String fname) { + for( int i=0; i<_fields.length; i++ ) + if( _fields[i]._fname==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 + TypeStruct xmeet(Type t) { + TypeStruct that = (TypeStruct) t; + assert !isFree() && !that.isFree(); + if( this==TOP ) return that; + if( that==TOP ) return this; + if( this==BOT ) return BOT; + if( that==BOT ) return BOT; + + // if equal, no matters. + // if short is BOT, chop ; recurPre on short. + // if short is TOP, copy long; recurPre on long. + TypeStruct min = _fields.length < that._fields.length ? this : that; + if( _fields.length != that._fields.length && (!min._open ^ min!=this) ) + return that.xmeet(this); + + assert (_name==that._name) == (_name.equals(that._name)); // Strings are interned + String name = + that._name==TOPNAME ? _name : + _name==TOPNAME ? that._name : + that._name==_name ? _name : + BOTNAME; + if( name==BOTNAME ) return BOT; + + // Check all other fields are sanely similar; same struct type but + // different field contents (e.g. field "age" is either 'int' or '18'). + int len = Math.min(_fields.length,that._fields.length); + for( int i=0; i<len; i++ ) { + Field f0 = _fields[i], f1 = that._fields[i]; + if( !f0._fname.equals(f1._fname) || f0._alias != f1._alias || f0._extern != f1._extern ) + return BOT; + } + + // See if we get a mid-recursion hit; breaks cycles + Integer pid = pid(that); + TypeStruct ts = (TypeStruct)VISIT.get(pid); + if( ts!=null ) return ts; + // If not mid-recursion, we are now; flag recursion. + boolean notRecur = VISIT.isEmpty(); + if( notRecur ) recurOpen(); + + // Setup and install the type, prior to recursing, so we can find our + // recursive self again. + ts = recurPre(pid,name,_open | that._open,_fref & that._fref); + + // Recurse all common fields + Field[] flds = ts._fields; + for( int i=0; i<len; i++ ) { + Field f0 = _fields[i], f1 = that._fields[i]; + flds[i].setType(f0._t.meet(f1._t )); + flds[i]._final = f0._final | f1._final; + } + + // Handle extra fields via `_open` + for( int i=len; i<_fields.length; i++ ) + flds[i].setType(_fields[i]._t ); + + // Cleanup post-recursion; intern/install at cycle end + if( notRecur ) ts = (TypeStruct)ts.recurClose(); + return ts; + } + + @Override + TypeStruct xdual() { + Field[] flds = new Field[_fields.length]; + for( int i=0; i<_fields.length; i++ ) + flds[i] = _fields[i].dual(); + return malloc(sdual(),!_open,!_fref,flds); + } + + // Recursive dual + @Override TypeStruct rdual() { + if( _dual!=null ) return dual(); + assert !_terned; + Field[] flds = new Field[_fields.length]; + TypeStruct d = malloc(sdual(),!_open,!_fref,flds); + (_dual = d)._dual = this; // Cross link duals + for( int i=0; i<_fields.length; i++ ) + flds[i] = _fields[i]._terned ? _fields[i].dual() : _fields[i].rdual(); + return d; + } + + private String sdual() { + return + _name==TOPNAME ? BOTNAME : + _name==BOTNAME ? TOPNAME : + _name; + } + + @Override boolean _isConstant() { + assert !_open; // who is asking about constant open structs? if( _open ) return false; // Infinite BOT fields + assert !VISIT.containsKey(_uid); //expect constants to not include self-cycles. if( VISIT.containsKey(_uid) ) return true; // Cycles assume constant + VISIT.put(_uid,this); + // Check all fields for being constant + for( Field field : _fields ) + if( !field._isConstant() ) + return false; + return true; + } + + // All fields are final + @Override boolean _isFinal() { + if( _open ) return false; // May have more non-final fields + if( VISIT.containsKey(_uid) ) // Test: been here before? + return true; // Cycles assume final + VISIT.put(_uid,this); // Set: don't do this again + for( Field fld : _fields ) + if( !fld._isFinal() ) + return false; + return true; + } + + private TypeStruct recurPre(Object key, String name, boolean open, boolean fref) { + // Make a clone of original; suitable for hashing so can build + // e.g. TMPs to clone as part of cycles - but will fail the 'eq' check + // until the entire cycle is built. + Field[] flds = new Field[_fields.length]; + for( int i=0; i<flds.length; i++ ) + flds[i] = _fields[i].malloc(); // Blank copy, but can be hashed + TypeStruct ts = malloc(name, open, fref, flds ); + ts._aliases = aliases(); + VISIT.put(key,ts); + return ts; + } + + private interface FieldTransform { Type apply(Type t); } + + private TypeStruct recurTransform(Object key, String name, boolean open, boolean fref, boolean xfinal, FieldTransform transform) { + TypeStruct ts = (TypeStruct)VISIT.get(key); + if( ts!=null ) return ts; // Already visited + ts = recurPre(key,name,open,fref); + Field[] flds = ts._fields; + for( Field fld : flds ) + fld._final |= xfinal; + + // Now start the recursion + for( int i=0; i<flds.length; i++ ) + flds[i].setType(transform.apply(_fields[i]._t)); + + return ts; + } + + // Make a read-only version + @Override TypeStruct _makeRO() { + return recurTransform(_name,_name,_open,_fref,true,Type::_makeRO); + } + + @Override TypeStruct _makeStorage() { + return recurTransform(_uid,_name,_open,false,false,Type::_makeStorage); + } + + // log_size for a struct is not defined, unless its exactly some power of + // 2. *Total size* is well-defined, and is available in the offsets. + @Override public int log_size() { throw Utils.TODO("Should not reach here: structs have no scalar log size"); } + @Override public int size() { return offset(_fields.length); } + @Override public int alignment() { + int align = 0; + for( Field f : _fields ) + if( !f._extern ) align = Math.max(align, f._t.alignment()); + return align; + } + + // If false, always false. + // If true , maybe true, need to check recursive fields. + private boolean static_eq( TypeStruct ts ) { + assert (_name==ts._name) == (_name.equals(ts._name)); // Strings are interned + return _name==ts._name && _open==ts._open && _fref==ts._fref && _fields.length==ts._fields.length; + } + + @Override boolean eq(Type t) { + TypeStruct ts = (TypeStruct)t; // Invariant + + // Recursive; so use cyclic equals + if( !VISIT.isEmpty() ) { + assert CEQUALS.isEmpty(); + boolean rez = cycle_eq(t); + CEQUALS.clear(); + return rez; + } + // Normal equals + if( !static_eq(ts) ) return false; + if( _fields==ts._fields ) return true; + for( int i = 0; i < _fields.length; i++ ) + if( !_fields[i].eq(ts._fields[i]) ) + return false; + return true; + } + + @Override boolean cycle_eq(Type t) { + if( this==t ) return true; + TypeStruct ts = (TypeStruct)t; // Invariant + if( !static_eq(ts) ) return false; + if( _fields==ts._fields ) return true; + // Check to see if we've ever compared this pair of types before; + // if so, then assume the cycle is equal here. + int pid = pid(ts); + if( CEQUALS.find(pid)!= -1 ) return true; + CEQUALS.push(pid); + // Recursively check fields + for( int i = 0; i < _fields.length; i++ ) + if( !_fields[i].cycle_eq(ts._fields[i]) ) + return false; + return true; + } + + @Override + int hash() { + long hash = _name.hashCode() ^ (_open ? 4 : 0) ^ (_fref ? 8 : 0); + for( Field f : _fields ) + hash = Utils.rot(hash,13) ^ ((long)f._fname.hashCode() * f._alias); + return Utils.fold(hash); + } + + @Override public int nkids() { return _fields.length; } + @Override public Field at( int idx ) { return _fields[idx]; } + @Override public void set( int idx, Type t ) { _fields[idx] = (Field)t; } + // Tags 0-4 - closed +#nflds (+name) + // Tags 5 - closed(+ nflds +name) + // Tags 6-10- open +#nflds (+name) + // Tags 11 - open (+ nflds +name) + + + @Override + SB _print(SB sb, BitSet visit, boolean html ) { + if( this==BOT ) return sb.p("StructBot"); + if( this==TOP ) return sb.p("StructTop"); + if( isFree() ) return sb.p("FREE:").p(_name); + if( isAry() && field("[]")._t instanceof TypeConAry con ) + return sb.p(con.str()); + sb.p(_name); + if( isAry() ) + // Array names deliberately canonicalize mutable and immutable + // element variants. The verbose debugger print must retain the + // otherwise invisible lattice coordinate. + return sb.p(_fields[1]._final ? "[final]" : "[mutable]"); + if( html ) + return sb; + sb.p(" {"); + for( Field f : _fields ) + (f._t ==null ? sb.p("---") : f._t.print(sb,visit,html)).p(f._final ? " " : " !").p(f._fname).p("; "); + if( _open && !_fref ) sb.p("... "); + return sb.p("}"); + } + + @Override public String str() { + if( this==BOT ) return "StructBot"; + if( this==TOP ) return "StructTop"; + return (isFree() ? "FREE:":"")+_name; + } + + + public boolean isAry() { return find("[]")!=-1; } + public boolean isConAry() { + return isAry() && _fields[1]._t instanceof TypeConAry; + } + + public int aryBase() { + assert isAry(); + if( _offs==null ) _offs = offsets(); + return _offs[1]; + } + public int aryScale() { + assert isAry(); + return _fields[1]._t.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 + assert CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.FinalizeIR.ordinal(); + // Compute a layout for a collection of fields + assert _fields != null; // No forward refs + + // Compute a layout + _offs = new int[_fields.length+1]; + if( isAry() ) { // Arrays used fixed u32 len, padding to elems, var length + assert _fields[1]._fname=="[]"; + // Pad out to the element alignment + int align = _fields[1]._t.alignment(); + _offs[1] = (4 + ((1<<align)-1)) & -(1<<align); + if( _fields[1]._t instanceof TypeConAry ary ) // Array length is well-defined for constants, not defined for runtime arrays + _offs[2] = _offs[1]+ary.len(); + + } else { + int[] cnts = new int[5]; // Count of fields at log field size + int flen = _fields.length; + for( int i=0; i<flen; i++ ) + if( !_fields[i]._extern ) + cnts[_fields[i]._t.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. + for( int i=0; i<flen; i++ ) { + if( _fields[i]._extern ) { _offs[idx++] = -1; continue; } + int log = _fields[i]._t.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[flen] = off; // Max struct size, no trailing padding + } + return _offs; + } + + // Field order with increasing offset + public int[] layout() { + offset(0); + int len = _offs.length; + int[] is = new int[len]; + for( int i=0; i<len; i++ ) is[i] = i; + // Stoopid hand rolled bubble sort + for( int i=0; i<len; i++ ) + for( int j=i+1; j<len; j++ ) + if( _offs[is[j]] < _offs[is[i]] ) + { int tmp = is[i]; is[i] = is[j]; is[j] = tmp; } + return is; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeTuple.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeTuple.java new file mode 100644 index 00000000..0d70bdf2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/TypeTuple.java @@ -0,0 +1,158 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.*; +import java.util.*; + +public class TypeTuple extends Type { + + public Type[] _types; + + private static final Ary<TypeTuple> FREE = new Ary<>(TypeTuple.class); + private TypeTuple(Type[] types) { super(TTUPLE); _types = types; } + private TypeTuple init( Type[] types ) { _types = types; return this; } + private static TypeTuple malloc(Type[] types) { + return FREE.isEmpty() ? new TypeTuple(types) : FREE.pop().init(types); + } + // All fields directly listed + public static TypeTuple make(Type... types) { + TypeTuple tt = malloc(types); + TypeTuple t2 = tt.intern(); + return t2==tt ? tt : t2.free(tt); + } + @Override TypeTuple free(Type t) { + TypeTuple tt = (TypeTuple)t; + tt._types = null; + tt._dual = null; + tt._hash = 0; + FREE.push(tt); + return this; + } + @Override boolean isFree() { return _types==null; } + + public TypeTuple makeFrom(int idx, Type tx ) { + if( _types[idx]==tx ) return this; + Type[] types = _types.clone(); + types[idx] = tx; + return make(types); + } + public TypeTuple meetFrom(int idx, Type t ) { return makeFrom(idx,_types[idx].meet(t)); } + + public static final TypeTuple BOT = malloc(new Type[0]).intern(); + public static final TypeTuple TOP = BOT.dual(); + // The default state, a triple of CONTROL, Memory, and Value, with bottoms + // for all. This is the default StopNode type, when new functions are + // being created and we cannot tell what the future holds. This is the + // default ReturnNode type, returning what the worst a function can do. + public static final TypeTuple STATE = make(Type.CONTROL,TypeMem.BOT,Type.BOTTOM); + public static final TypeTuple STOP_HIGH = make(Type.CONTROL,TypeMem.START,Type.TOP); + + public static final TypeTuple TEST = make(TypeInteger.BOT,TypeMemPtr.TEST); + public static final TypeTuple DEAD_NEW = make(TypeMemPtr.TOP,TypeMem.TOP); + + public static final TypeTuple IF_BOTH = make(Type. CONTROL,Type. CONTROL); + public static final TypeTuple IF_NEITHER = make(Type.XCONTROL,Type.XCONTROL); + public static final TypeTuple IF_TRUE = make(Type. CONTROL,Type.XCONTROL); + public static final TypeTuple IF_FALSE = make(Type.XCONTROL,Type. CONTROL); + + public static void gather(ArrayList<Type> ts) { ts.add(BOT); ts.add(TEST); ts.add(STATE); ts.add(IF_TRUE); } + + @Override + Type xmeet(Type other) { + TypeTuple that = (TypeTuple)other; // contract from xmeet + if( this==TOP ) return that; + if( that==TOP ) return this; + if( _types.length != that._types.length ) + return BOT; + return make(TypeFunPtr.xmeet(_types,that._types)); + } + + @Override TypeTuple xdual() { + if( _types.length==0 ) return malloc(null); + boolean isCon=true; + for( Type t : _types ) + if( t._dual!=t ) + // Not a constant + return malloc(TypeFunPtr.xdual(_types)); + return this; + } + @Override TypeTuple rdual() { + if( _dual!=null ) return dual(); + assert !_terned; + assert _types.length>0; + Type[] ts = new Type[_types.length]; + TypeTuple d = malloc(ts); + (_dual = d)._dual = this; // Cross link duals + // Collection rdual + for( int i=0; i<_types.length; i++ ) + ts[i] = _types[i]._terned ? _types[i].dual() : _types[i].rdual(); + return d; + } + + @Override boolean _isConstant() { + for( Type t : _types ) + if( !t._isConstant() ) + return false; + return true; + } + @Override boolean _isFinal() { throw Utils.TODO("Should not reach here: tuples are not stored object values"); } + + @Override TypeMemPtr _makeRO() { throw Utils.TODO("Should not reach here: tuples cannot be made read-only"); } + + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + Type[] ts = new Type[_types.length]; + for( int i=0; i<ts.length; i++ ) + ts[i] = _types[i]._upgradeType(TYPES); + return make(ts); + } + + @Override public int log_size() { throw Utils.TODO("Should not reach here: tuples have no scalar storage size"); } + @Override public int alignment() { + assert isConstant(); + int align = 0; + for( Type t : _types ) + align = Math.max(align,t.alignment()); + return align; + } + + public Type ctl() { assert _types.length>=3; return _types[0]; } + public Type ret() { assert _types.length>=3; return _types[2]; } + public TypeMem mem() { assert _types.length>=3; + return _types[1] instanceof TypeMem tmem ? tmem : (_types[1].isHigh() ? TypeMem.START : TypeMem.BOT); + } + + @Override public int nkids() { return _types.length; } + @Override public Type at( int idx ) { return _types[idx]; } + @Override public void set( int idx, Type t ) { _types[idx] = t; } + + + @Override SB _print(SB sb, BitSet visit, boolean html ) { + if( this==TOP ) return sb.p("[TOP]"); + if( this==BOT ) return sb.p("[BOT]"); + sb.p("[ "); + for( Type t : _types ) + t.print(sb,visit,html).p(", "); + return sb.unchar(2).p("]"); + } + + @Override public String str() { return print(new SB(),new BitSet(),false).toString(); } + + @Override + int hash() { + int sum = 0; + if( _types!=null ) for( Type type : _types ) sum ^= type.hashCode(); + return sum; + } + + @Override + boolean eq( Type t ) { + TypeTuple that = (TypeTuple)t; // Contract + if( _types==null && that._types==null ) return true; + if( _types==null || that._types==null ) return false; + if( _types.length != that._types.length ) return false; + for( int i=0; i<_types.length; i++ ) + if( _types[i]!=that._types[i] ) + return false; + return true; + } + +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/type/XInt.java b/chapter-modules/src/main/java/com/seaofnodes/simple/type/XInt.java new file mode 100644 index 00000000..126a4e64 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/type/XInt.java @@ -0,0 +1,244 @@ +package com.seaofnodes.simple.type; + +import com.seaofnodes.simple.util.*; +import java.lang.Integer; +import java.util.Arrays; + +// Stupidly overkill interned immutable int[]'s +public abstract class XInt { + private static final Ary<Ary<int[]>> FREES = new Ary(Ary.class); + private static final Ary<Ary<int[]>> INTERNS = new Ary(Ary.class); + private static int PROBES, HITS, NEWS; + + public static final int[] EMPTY = intern(new int[]{0, 0}); + public static final int[] FULL = intern(new int[]{0,-1}); + public static final int[] SET1 = make(1); + public static final int[] SET3 = make(SET1, 2); + + static int idx(int x) { return (x>>5)+1; } + static int mask(int x) { return 1<<(x&31); } + + // No hash set, still mutable + public static boolean isMutable(int[] xs) { return xs[0]==0; } + // Return hash code; must be set hence frozen + public static int hash(int[] xs) { return xs[0]; } + + // High bit set (infinite 1's extension) + public static boolean isHigh(int[] xs) { return xs[xs.length-1] < 0; } + + // Check for a bit + public static boolean bit(int[] xs, int x) { + int idx = idx(x); + if( idx >= xs.length ) + return isHigh(xs); + return (xs[idx] & mask(x))!=0; + } + + // Number of bits set + public static int bitCount(int[] xs) { + assert !isHigh(xs); // Dont ask if infinite + int cnt=0; + for( int i=1; i<xs.length; i++ ) + cnt += Integer.bitCount(xs[i]); + return cnt; + } + + // Largest bit + public static int max(int[] xs) { + assert !isHigh(xs); + return Integer.numberOfTrailingZeros(xs[xs.length-1])+((xs.length-1-1)<<5); + } + + // Has 1 bit set + public static boolean isConstant(int[] xs) { return !isHigh(xs) && bitCount(xs)==1; } + // Return the 1 set bit or assert + public static int onlyBit(int[] xs) { + assert isConstant(xs); + for( int i=1; i<xs.length; i++ ) + if( xs[i] != 0 ) + return Integer.numberOfTrailingZeros(xs[i]) + ((i-1)<<5); + throw Utils.TODO("Should not reach here"); + } + + // Subset check: ys is a subset of xs + public static boolean subset(int[] xs, int[] ys) { + assert !isMutable(ys); + if( xs==ys ) return true; + if( xs==FULL ) return true; + if( ys==EMPTY ) return true; + int min = Math.min(xs.length,ys.length); + for( int i=1; i<min; i++ ) + if( (xs[i] | ys[i])!=xs[i] ) + return false; + if( xs.length == ys.length ) + return true; + if( ys.length < xs.length ) + return !isHigh(ys); // If ys is infinite, it'll hit one of the xs extra zeros + return isHigh(xs); + } + + + // Construct a set with 1 bit set + public static int[] make(int x ) { return make(EMPTY,x); } + + // Add a bit + public static int[] make(int[] xs, int x ) { + assert !isMutable(xs); + int idx = idx(x); + if( idx >= xs.length && isHigh(xs) ) + return xs; // Set in the default infinite + if( idx < xs.length && (mask(x) & xs[idx])!=0 ) + return xs; // Already set + int len = Math.max(xs.length,idx+1); + if( (x&31)==31 && idx+1==len ) + len++; + int[] ys = free(len); + System.arraycopy(xs,1,ys,1,xs.length-1); + ys[idx] |= mask(x); + return intern(ys); + } + + // Interning, and life-cycle management + // Intern the array + public static int[] intern(int[] xs) { + if( !isMutable(xs) ) return xs; // Already interned + PROBES++; + + // Trim trailing 0s or -1s, use the infinite extend bit + int len = xs.length; + while( len > 2 && + ((xs[len-1]== 0 && xs[len-2]>=0) || + (xs[len-1]== -1 && xs[len-2]< 0) ) ) + len--; + // Replace with trimmed + if( len < xs.length ) { + var ys = free(len); + System.arraycopy(xs,1,ys,1,len-1); + xs = _free(xs,ys); + } + + // Compute lame hash code + int hash = 0; + for( int i=1; i<len; i++ ) + hash += xs[i]; + if( hash == 0 ) hash = 0xCAFEBABE; + xs[0] = hash; // Set lame hashcode, and mark immutable + + // Get length-specific INTERN table + var xss = INTERNS.atX(len); + if( xss==null ) + INTERNS.setX( xs.length, xss = new Ary<>(int[].class)); + + // Probe for a hit + for( int[] ys : xss ) + if( Arrays.equals(xs,ys) ) // Hit! + // Free new array and return old array + { HITS++; return _free(xs,ys); } + + // Intern new array and return + return xss.push(xs); + } + + // Return Ary of free int[]s of fixed size + private static Ary<int[]> _xss(int len) { + Ary<int[]> xss = FREES.atX(len); + if( xss==null ) + FREES.setX(len, xss = new Ary<>(int[].class)); + return xss; + } + // Pull free array from free list + static int[] free(int len) { + int[] xs = _xss(len).pop(); + if( xs==null ) + { NEWS++; xs = new int[len]; } + assert isMutable(xs); + return xs; + } + // Return ys. Put xs on free list + static int[] _free(int[] xs, int[] ys) { + Arrays.fill(xs,0); // Mutable again + _xss(xs.length).push(xs); + return ys; + } + + // Meet: Set union + public static int[] meet(int[] xs, int[] ys) { + if( xs==ys ) return xs; + boolean isX=true, isY=true; + int min = Math.min(xs.length,ys.length); + int max = Math.max(xs.length,ys.length); + for( int i=1; i<max; i++ ) { + int x = i < xs.length ? xs[i] : isHigh(xs) ? -1 : 0; + int y = i < ys.length ? ys[i] : isHigh(ys) ? -1 : 0; + if( (x | y)!=x ) isX=false; + if( (x | y)!=y ) isY=false; + } + if( isX ) return xs; + if( isY ) return ys; + // Get a free one + int[] ms = free(max); + for( int i=1; i<min; i++ ) + ms[i] = xs[i] | ys[i]; + for( int i=min; i<ms.length; i++ ) { + int x = i < xs.length ? xs[i] : isHigh(xs) ? -1 : 0; + int y = i < ys.length ? ys[i] : isHigh(ys) ? -1 : 0; + ms[i] = x | y; + } + return intern(ms); + } + + // Dual: flip all the bits + public static int[] dual(int[] xs) { + int[] ds = free(xs.length); + for( int i=1; i<xs.length; i++ ) + ds[i] = ~xs[i]; + return intern(ds); + } + + + // Next set bit, or -1 + // for( int bit = XInt.next(bits,0); bit >=0; bit = XInt.next(bits,bit) ) {...bit...} + public static int next(int[] xs, int x) { + assert !isHigh(xs); // No iterating infinite bitsets + x++; // Skip current + for( ; !bit(xs,x); x++ ) + if( idx(x)>=xs.length ) + return -1; + return x; + } + + // Remap all bits + + + public static String str(int[] xs) { return str(new SB(),xs).toString(); } + public static SB str(SB sb, int[] xs) { + sb.p("[ "); + // Printing ranges; + // 1,2,3 prints 1-3 + // 1,2,4 prints 1,2,4 + // + int max = (xs.length-1)<<5; + int low = 0; // No low range + for( int i=1; i<max; i++ ) { + if( bit(xs,i) ) { // Set bit, start or extend range + if( low==0 ) low=i; // No range, start one + } else { // Break in sequence, ending a range + range(sb,low,i-1); // Print prior range, and start new one + low=0; // Reset range + } + } + // Print any partial range + range(sb,low,max-1).unchar(); + if( isHigh(xs) ) + sb.p(",..."); + return sb.p("]"); + } + + private static SB range( SB sb, int low, int mid ) { + if( low==0 ) ; + else if( low==mid ) sb.p(low).p(','); + else if( low==mid-1 ) sb.p(low).p(',').p(mid).p(','); + else sb.p(low).p('-').p(mid).p(','); + return sb; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/util/Ary.java b/chapter-modules/src/main/java/com/seaofnodes/simple/util/Ary.java new file mode 100644 index 00000000..d67801c2 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/util/Ary.java @@ -0,0 +1,229 @@ +package com.seaofnodes.simple.util; + +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]; + } + public E atX( int i ) { + if( i >= _len ) return null; + return _es[i]; + } + + /** @return last element */ + public E last( ) { return at(_len-1); } + public E last(int x ) { return at(_len-1+x); } + + /** 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; + } + + /** Slow, linear-time, element insertion. Preserves order. + * @param i where to insert + * @return element inserted */ + public E insert( E e, int i ) { + setLen(_len+1); + System.arraycopy(_es,i,_es,i+1,_len-i); + _es[i] = e; + 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); } + + /** 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/chapter-modules/src/main/java/com/seaofnodes/simple/util/AryInt.java b/chapter-modules/src/main/java/com/seaofnodes/simple/util/AryInt.java new file mode 100644 index 00000000..b498fd78 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/util/AryInt.java @@ -0,0 +1,192 @@ +package com.seaofnodes.simple.util; + +import java.util.Arrays; +import java.util.Collection; +import java.util.function.IntUnaryOperator; + +// ArrayList with saner syntax +public class AryInt { + public int[] _es; + public int _len; + public AryInt(int[] es) { this(es,es.length); } + public AryInt(int[] es, int len) { _es=es; _len=len; } + public AryInt() { this(new int[1],0); } + + /** @return list is empty */ + public boolean isEmpty() { return _len==0; } + /** @return active list length */ + public int len() { return _len; } + /** @param i element index + * @return element being returned; throws if OOB */ + public int at( int i ) { + range_check(i); + return _es[i]; + } + /** @param i element index + * @return element being returned, or 0 if OOB */ + public int atX( int i ) { + return i < _len ? _es[i] : 0; + } + /** @return last element */ + public int last( ) { return at(_len-1); } + public int last(int x ) { return at(_len-1+x); } + + /** @return remove and return last element */ + public int pop( ) { + range_check(0); + return _es[--_len]; + } + + /** Add element in amortized constant time + * @param e element to add at end of list + * @return 'this' for flow-coding */ + public AryInt push( int e ) { + if( _len >= _es.length ) _es = Arrays.copyOf(_es,Math.max(1,_es.length<<1)); + _es[_len++] = e; + return this; + } + + /** Slow, linear-time, element insert. Preserves order. + * @param i index to insert at, between 0 and _len inclusive. + * @param e intlement to insert + */ + public void insert( int i, int e ) { + if( i < 0 || i>_len ) + throw new ArrayIndexOutOfBoundsException(""+i+" >= "+_len); + if( _len >= _es.length ) _es = Arrays.copyOf(_es,Math.max(1,_es.length<<1)); + System.arraycopy(_es,i,_es,i+1,(_len++)-i); + _es[i] = e; + } + + /** Fast, constant-time, element removal. Does not preserve order + * @param i element to be removed + * @return element removed */ + public int del( int i ) { + range_check(i); + int 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 */ + public int remove( int i ) { + range_check(i); + int e = _es[i]; + System.arraycopy(_es,i+1,_es,i,(--_len)-i); + return e; + } + + /** Remove all elements */ + public void clear( ) { Arrays.fill(_es,0,_len,0); _len=0; } + + // Extend and set + public int setX( int i, int e ) { + while( i>= _es.length ) _es = Arrays.copyOf(_es,_es.length<<1); + if( i >= _len ) _len = i+1; + return (_es[i] = e); + } + + public int set( int i, int e ) { + range_check(i); + return (_es[i] = e); + } + + public AryInt set_as( int e ) { _es[0] = e; _len=1; return this; } + public AryInt set_len( 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); + return this; + } + + /** @param c Collection to be added */ + public AryInt addAll( Collection<? extends Integer> c ) { for( int e : c ) push(e); return this; } + + /** @param es Array to be added */ + public AryInt addAll( int[] es ) { + if( 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 es Array to be added */ + public AryInt addAll( AryInt ary ) { + if( ary._len==0 ) return this; + while( _len+ary._len > _es.length ) _es = Arrays.copyOf(_es,_es.length<<1); + System.arraycopy(ary._es,0,_es,_len,ary._len); + _len += ary._len; + return this; + } + + public AryInt map_update( IntUnaryOperator f ) { for( int i = 0; i<_len; i++ ) _es[i] = f.applyAsInt(_es[i]); return this; } + + /** @return compact array version, using the internal base array where possible. */ + public int[] asAry() { return _len==_es.length ? _es : Arrays.copyOf(_es,_len); } + + /** Sorts in-place */ + public void sort_update() { Arrays.sort(_es, 0, _len); } + /** 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 intlement to find + * @return index of first matching element, or -1 if none */ + public int find( int e ) { + for( int i=0; i<_len; i++ ) if( _es[i]==e ) return i; + return -1; + } + + @Override public String toString() { + SB sb = new SB().p('['); + for( int i=0; i<_len; i++ ) + sb.p(_es[i]).p(','); + return sb.unchar().p(']').toString(); + } + + private void range_check( int i ) { + if( i < 0 || i>=_len ) + throw new ArrayIndexOutOfBoundsException(""+i+" >= "+_len); + } + + // Binary search sorted _es. Returns insertion point. + // Undefined results if _es is not sorted. + public int binary_search( int e ) { + int lo=0, hi=_len-1; + while( lo <= hi ) { + int mid = (hi + lo) >>> 1; // midpoint, rounded down + int mval = _es[mid]; + if( e==mval ) { + // If dups, get to the first. + while( mid>0 && e==_es[mid-1] ) mid--; + return mid; + } + if( e >mval ) lo = mid+1; + else hi = mid-1; + } + return lo; + } + + // 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 AryInt 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] != ary._es[i] ) + return false; + return true; + } + @Override public int hashCode( ) { + int sum=_len; + for( int i=0; i<_len; i++ ) + sum += _es[i]; + return sum; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/util/BAOS.java b/chapter-modules/src/main/java/com/seaofnodes/simple/util/BAOS.java new file mode 100644 index 00000000..61ce7387 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/util/BAOS.java @@ -0,0 +1,162 @@ +package com.seaofnodes.simple.util; + +import java.util.HashMap; +import java.util.Arrays; + +// "ByteArrayOutputStream", and Input Stream and not synchronized and has +// matching pack/unpack strategies. +public class BAOS { + + private byte[] _buf; + private int _len; + + public BAOS() { _buf = new byte[1]; } + public BAOS(byte[] buf) { _buf = buf; } + public BAOS(byte[] buf, int x) { _buf = buf; _len = x; } + + public byte[] toByteArray() { + return Arrays.copyOf(_buf,_len); + } + + public byte[] buf() { return _buf; } + public int size() { return _len; } + public void set( byte[] buf, int len ) { _buf=buf; _len=len; } + public void clear() { _len=0; } + + // ----------------------------------------------------------------------- + // Basic read, write (expand) + + private void grow(int len) { + while( _len+len >= _buf.length ) + _buf = Arrays.copyOf(_buf,_buf.length*2); + } + + // Write a byte, grow as needed; silent chop + public BAOS write(int b) { + grow(1); + _buf[_len++] = (byte)b; // Silent chop + return this; + } + + // Read an UNSIGNED bytes at INDEX, AIOOBE if off end + public int read1(int idx) { return _buf[idx] & 0xFF; } + + // Read an UNSIGNED byte, AIOOBE if off end + public int read() { return read1(_len++); } + + // write 2 bytes + public BAOS write2( int x ) { + grow(2); + _buf[_len++] = (byte)(x>> 0); + _buf[_len++] = (byte)(x>> 8); + return this; + } + + // Read a u16 in 2 bytes + public int read2(int idx) { return read1(idx) | (read1(idx+1)<<8); } + public int read2() { int rez = read2(_len); _len+=2; return rez; } + + // write 4 bytes + public BAOS write4( int x ) { + grow(4); + _buf[_len++] = (byte)(x>> 0); + _buf[_len++] = (byte)(x>> 8); + _buf[_len++] = (byte)(x>>16); + _buf[_len++] = (byte)(x>>24); + return this; + } + + // Read a i32 in 4 bytes + public int read4(int idx) { return read2(idx) | (read2(idx+2)<<16); } + public int read4() { int rez = read4(_len); _len+=4; return rez; } + + // write 8 bytes + public BAOS write8( long x ) { + write4((int) x ); + write4((int)(x>>32)); + return this; + } + + // Read a i64 in 8 bytes + public long read8(int idx) { return ((long)read4(idx) & 0xFFFFFFFFL) | ((long)read4(idx+4)<<32); } + public long read8() { long rez = read8(_len); _len+=8; return rez; } + + // Write byte array + public void write( byte[] bs ) { + grow(bs.length); + System.arraycopy(bs,0,_buf,_len,bs.length); + _len += bs.length; + } + + // Read/fill byte array + public byte[] read( byte[] bs ) { + System.arraycopy(_buf,_len,bs,0,bs.length); + _len += bs.length; + return bs; + } + + // ----------------------------------------------------------------------- + // Write a packed integer to the BAOS. Track a histogram to make more + // intelligent choices in the future. + private static final HashMap<Long,Integer> PACKED_HISTOGRAM = new HashMap<>(); + private BAOS histo(long x) { + Integer cnt = PACKED_HISTOGRAM.get(x); + PACKED_HISTOGRAM.put(x,cnt==null ? 1 : cnt+1); + return this; + } + + // Write a u8 (checked) + public void packed1(int x) { + assert 0 <= x && x <= 255; + histo(x).write(x); + } + + public int packed1() { return read(); } + + // Write a u16 (checked) + public void packed2(int x) { + assert 0 <= x && x <= 65535; + (( x <= 254 ) ? write(x) : write(0xFF).write2(x)).histo(x); + } + + // Read a packed u16 + public int packed2() { + int x = read(); + return x==0xFF ? read2() : x; + } + + // Write a packed i32 + public void packed4(int x) { + ((-127 <= x && x <= 127 ) ? write(x) : write(-128).write4(x)).histo(x); + } + + // Read a packed i32 + public int packed4() { + int x = _buf[_len++]; // Signed byte read + return x== -128 ? read4() : x; + } + + // Write a packed i64 + public void packed8(long x) { + ((-127 <= x && x <= 127 ) ? write((int)x) : write(-128).write8(x)).histo(x); + } + + // Read a packed i64 + public long packed8() { + int x = _buf[_len++]; // Signed byte read + return x== -128 ? read8() : x; + } + + public void packedS(String s) { + packed4(s.length()); + for( int i=0; i<s.length(); i++ ) + packed4(s.charAt(i)); + } + + public String packedS() { + int len = packed4(); + String s = new String(_buf,_len,len).intern(); + _len += len; + return s; + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/util/IntHashMap.java b/chapter-modules/src/main/java/com/seaofnodes/simple/util/IntHashMap.java new file mode 100644 index 00000000..b356ec31 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/util/IntHashMap.java @@ -0,0 +1,87 @@ +package com.seaofnodes.simple.util; + +import java.util.Arrays; + +// Int-Int Hash Map +public class IntHashMap { + private int[] _es; + private int _size; + private final int _sentinel; + + // Pick a sentinel value; not allowed for keys or values. Sentinel is + // returned on a miss. Writing sentinel counts as a remove. + public IntHashMap(int sentinel) { + _sentinel = sentinel; + _es = new int[4]; + Arrays.fill(_es,sentinel); + } + + public int size() { return _size; } + public boolean isEmpty() { return _size==0; } + public void clear() { + _size = 0; + Arrays.fill(_es,_sentinel); + } + + private int cap() { return _es.length>>1; } + + public int put( int key, int val ) { + int slot = probe(key); + int old = _es[(slot<<1)+1]; + if( old == _sentinel && val != _sentinel ) _size++; + if( old != _sentinel && val == _sentinel ) _size--; + // Check cap, resize at 3/4 full + if( _size > ((cap()>>1) + (cap()>>2)) ) { + grow(); + slot = probe(key); + } + + _es[ slot<<1 ] = key; + _es[(slot<<1)+1] = val; + return old; + } + + public int get( int key ) { + int slot = probe(key); + return _es[(slot<<1)+1]; + } + + private int probe( int key ) { + int mask = cap()-1; + int slot = key & mask; + while( _es[slot<<1] != _sentinel && _es[slot<<1] != key ) + slot = (slot + (key|1)) & mask; + return slot; + } + + // Double underlying array size + private void grow() { + int[] old = _es; + _es = new int[_es.length<<1]; + Arrays.fill(_es,_sentinel); + for( int i=0; i<old.length; i+=2 ) + if( old[i] != _sentinel ) { + int slot = probe(old[i]); + _es[ slot<<1 ] = old[i ]; + _es[(slot<<1)+1] = old[i+1]; + } + } + + // Just all the keys + public int[] keySet() { + int[] keys = new int[_size]; + int j=0; + for( int i=0; i<_es.length; i+=2 ) + if( _es[i] != _sentinel ) + keys[j++] = _es[i]; + return keys; + } + + @Override public String toString() { + SB sb = new SB().p("[ "); + for( int i=0; i<_es.length; i+=2 ) + if( _es[i] != _sentinel ) + sb.p('(').p(_es[i]).p(',').p(_es[i+1]).p(')').p(','); + return sb.unchar().p(']').toString(); + } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/util/SB.java b/chapter-modules/src/main/java/com/seaofnodes/simple/util/SB.java new file mode 100644 index 00000000..cc59f2a6 --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/util/SB.java @@ -0,0 +1,99 @@ +package com.seaofnodes.simple.util; + +/** 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; } + // 1 byte, 2 hex digits, 8 bits + public SB hex1(int s) { + int digit = (s>>4) & 0xf; + _sb.append((char)((digit <= 9 ? '0' : ('A'-10))+digit)); + digit = s & 0xf; + _sb.append((char)((digit <= 9 ? '0' : ('A'-10))+digit)); + return this; + } + // 2 bytes, 4 hex digits, 16 bits, Big Endian + public SB hex2(int s) { return hex1(s>> 8).hex1(s); } + // 4 bytes, 8 hex digits, 32 bits, Big Endian + public SB hex4(int s) { return hex2(s>>16).hex2(s); } + // 8 bytes, 16 hex digits, 64 bits, Big Endian + public SB hex8(long s) { return hex4((int)(s>>32)).hex4((int)s); } + + // Fixed width field + public SB fix( int sz, String s ) { + for( int i=0; i<sz; i++ ) + _sb.append( i < s.length() ? s.charAt(i) : ' '); + return this; + } + public char at(int idx ) { return _sb.charAt(idx); } + + // 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 setLen(int len) { _sb.setLength(len); return this; } + public String subString(int start, int end ) { return _sb.substring(start,end); } + + public SB clear() { _sb.setLength(0); return this; } + public int len() { return _sb.length(); } + @Override public String toString() { return _sb.toString(); } +} diff --git a/chapter-modules/src/main/java/com/seaofnodes/simple/util/Utils.java b/chapter-modules/src/main/java/com/seaofnodes/simple/util/Utils.java new file mode 100644 index 00000000..577a60af --- /dev/null +++ b/chapter-modules/src/main/java/com/seaofnodes/simple/util/Utils.java @@ -0,0 +1,53 @@ +package com.seaofnodes.simple.util; + +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/chapter-modules/src/main/smp/sys.smp b/chapter-modules/src/main/smp/sys.smp new file mode 100644 index 00000000..7705a187 --- /dev/null +++ b/chapter-modules/src/main/smp/sys.smp @@ -0,0 +1,11 @@ + +// top-level default standard library import + +val adt = adt; // Abstract Data Types +val ary = ary; // Array utilities +val aryi64 = aryi64; // Variable sized array of i64 +val aryu8 = aryu8; // Variable sized array of u8 +val char = char; // Character utilities +val io = io; // IO library +val libc = libc; // libc FFI bindings +val scan = scan; // A simple string parser diff --git a/chapter-modules/src/main/smp/sys/adt.smp b/chapter-modules/src/main/smp/sys/adt.smp new file mode 100644 index 00000000..a4fe40bf --- /dev/null +++ b/chapter-modules/src/main/smp/sys/adt.smp @@ -0,0 +1,3 @@ + +// Abstract data types. +val bitset = bitset; diff --git a/chapter-modules/src/main/smp/sys/adt/bitset.smp b/chapter-modules/src/main/smp/sys/adt/bitset.smp new file mode 100644 index 00000000..d02cd2fe --- /dev/null +++ b/chapter-modules/src/main/smp/sys/adt/bitset.smp @@ -0,0 +1,57 @@ + +// A set of non-negative integers, represented one bit per value. +struct bitset { + i64[] !_bits; + + new bitset = { -> + _bits = new i64[1]; + }; + + // Return 0 for missing, non-zero for true + val get = { int bit -> + if( (bit >> 6) >= _bits# ) return false; + i64 mask = 1 << (bit & 63); + return (_bits[bit >> 6] & mask); + }; + + val _grow = { int bit -> + int x = bit >> 6; + int len = _bits#; + if( x < len ) return self; + while( x >= len ) len *= 2; + var bits = new i64[len]; + for( int i=0; i<_bits#; i++ ) + bits[i] = _bits[i]; + _bits = bits; + return self; + }; + + // Set + val set = { int bit -> + i64 mask = 1 << (bit & 63); + _grow(bit)._bits[bit >> 6] |= mask; + return self; + }; + + // Test and set + val tset = { int bit -> + int idx = bit >> 6; + i64 mask = 1 << (bit & 63); + if( _grow(bit)._bits[idx] & mask ) + return true; // Already set + _bits[idx] |= mask; + return false; // Was false (and now set) + }; + + val clr = { int bit -> + if( (bit >> 6) >= _bits# ) return self; + i64 mask = 1 << (bit & 63); + _bits[bit >> 6] &= (mask ^ -1); + return self; + }; + + val clear = { -> + for( int i=0; i<_bits#; i++ ) _bits[i] = 0; + return self; + }; +}; diff --git a/chapter-modules/src/main/smp/sys/ary.smp b/chapter-modules/src/main/smp/sys/ary.smp new file mode 100644 index 00000000..8312e94c --- /dev/null +++ b/chapter-modules/src/main/smp/sys/ary.smp @@ -0,0 +1,24 @@ + +// Array utilities + +// Copy byte array contents. Copies the shorter of the buffers and len +val copy = { u8[] dst, int doff, u8[~] src, int soff, int len -> + if( len > dst# - doff ) len = dst# - doff; + if( len > src# - soff ) len = src# - soff; + for( int i=0; i<len; i++ ) + dst[i+doff] = src[i+soff]; + return dst; +}; + +// Compare lexigraphically array contents. Compares up to shorter array or +// len. Returns <0 or >0 for first non-matching byte. Returns <0 or >0 for +// the larger array after that, or 0 if both arrays are fully equal. +val cmp = { u8[~] src1, u32 off1, u8[~] src2, u32 off2, u32 len -> + u32 len1 = len > src1# - off1 ? src1# - off1 : len; + u32 len2 = len > src2# - off2 ? src2# - off2 : len; + u32 max = len1 > len2 ? len2 : len1; + for( int i=0; i<max; i++ ) + if( src1[off1+i] != src2[off2+i] ) + return src1[off1+i] - src2[off2+i]; // Bytes determine compare + return len1 - len2; // Length determines compare +}; diff --git a/chapter-modules/src/main/smp/sys/aryi64.smp b/chapter-modules/src/main/smp/sys/aryi64.smp new file mode 100644 index 00000000..98b72605 --- /dev/null +++ b/chapter-modules/src/main/smp/sys/aryi64.smp @@ -0,0 +1,82 @@ + +// Ary/Vector of i64s +struct aryi64 { + u32 !len = 0; + i64[] !buf; + + new aryi64 = { i64[] b -> + buf = b; + }; + + // Grow as needed + val _grow = { int len2 -> + if( len+len2 < buf# ) return self; + int newlen = buf# * 2; + while( len+len2 >= newlen ) newlen *= 2; + i64[] !buf2 = new i64[newlen]; + for( int i=0; i<buf#; i++ ) + buf2[i] = buf[i]; + buf = buf2; + self; + }; + + // Add a i64 to aryu8 + val add = { i64 c -> + _grow(1).buf[len++] = c; + self; + }; + + val get = { int index -> return buf[index]; }; + + // Write the bracketed array-list form to aryu8. + val str = { -> + // TODO: clunky default constructor syntax + var str = new aryu8().add('['); + for( int i=0; i<len; i++ ) + str.num(buf[i]).add(',').add(' '); + if( len ) str.undo(2); + return str.add(']'); + }; + + val swap = { int i, int j -> + i64 tmp = buf[i]; + buf[i] = buf[j]; + buf[j] = tmp; + self; + }; +}; + + + +// Parse a comma-separated list of numbers, wrapped with square brackets, +// with optional whitespace e.g. "[ -17, 0,99,12456789 ] ". +// parseAryI64 & aryi64.str are bijections modulo whitespace. +// Returns null if the parse failed, without giving a clue as to why. +val parseAryI64 = { u8[~] str -> + !scan !s = new scan(str); + if( !s.match('[') ) return null; + // Array of parsed results + var nums = new aryi64(new i64[1]); + + // Parse a list of numbers + if( s.match(']') ) // Handle the empty list + return nums; + + // Parse number commas "123, " until something else + while( s.x < s.buf# ) { + nums.add(s.number()); + if( s.bad ) return null; + if( !s.match(',') ) + break; + }; + // Close the list + if( !s.match(']') ) return null; + s.skip(); + return s.x == s.buf# ? nums : null; +}; + +val copy = { i64[] dst, int doff, i64[~] src, int soff, int len -> + for( int i=0; i<len; i++ ) + dst[i+doff] = src[i+soff]; + return dst; +}; diff --git a/chapter-modules/src/main/smp/sys/aryu8.smp b/chapter-modules/src/main/smp/sys/aryu8.smp new file mode 100644 index 00000000..21cf920b --- /dev/null +++ b/chapter-modules/src/main/smp/sys/aryu8.smp @@ -0,0 +1,87 @@ + +// Ary/Vector of u8s - a resizable array of bytes. +struct aryu8 { + u32 !len = 0; + u8[] !buf; + + new aryu8 = { -> buf = new u8[1]; }; + + // Grow as needed + val _grow = { int len2 -> + if( len+len2 < buf# ) return self; + int newlen = buf# * 2; + while( len+len2 >= newlen ) newlen *= 2; + u8[] !buf2 = new u8[newlen]; + ary.copy(buf2,0,buf,0,buf#); + buf = buf2; + self; + }; + + val clear = { -> len = 0; self; }; + + // Add a character to aryu8 + val add = { u8 c -> + _grow(1).buf[len++] = c; + self; + }; + + // Add all of a plain array to the aryu8 + val addAry = { u8[~] xs -> + _grow(xs#); + ary.copy(buf,len,xs,0,xs#); + len += xs#; + self; + }; + + // Remove x bytes from the tail. + val undo = { int x -> + len = len >= x ? len-x : 0; + self; + }; + + // Print ASCII number into aryu8 + val num = { i64 x -> + if( !x ) { add('0'); return self; } + if( x < 0 ) { add('-'); x = -x; } + i64 ten = 1, log = 0; + while( ten <= x ) { ten *= 10; log++; } + for( int i=0; i<log; i++ ) { + ten /= 10; + i64 q = x/ten; + u8 digit = q+'0'; + add(digit); + x -= q*ten; + } + self; + }; + + // Write to fd, returns the number of bytes written (len). + val write = { i32 fd -> + i64 ptr = buf; // Cast buffer pointer to i64 for native C call + libc.write(fd,ptr,len); + }; +}; + + +// Parse one integer, ignore trailing data; returns 0 with an empty string. +// If you want a more robust parsing, use the `scan` class +val atoi = { u8[~] str -> + int x = 0; + while( x < str# && str[x] <= ' ' ) x++; + if( x == str# ) return 0; + bool neg = false; + if( str[x]=='-' ) { neg=true; x++; } + i64 n = 0; + while( x < str# && '0' <= str[x] <= '9' ) + n = n*10 + str[x++]-'0'; + return neg ? -n : n; +}; + +// Zero-terminate string for libc strings. No error checking, just copies and +// appends a zero. +{ u8[~] -> u8[] } str0 = { u8[~] str -> + u8[] !strz = new u8[str#+1]; + ary.copy(strz,0,str,0,str#); + strz[str#] = '\0'; + strz; +}; diff --git a/chapter-modules/src/main/smp/sys/char.smp b/chapter-modules/src/main/smp/sys/char.smp new file mode 100644 index 00000000..22ca4213 --- /dev/null +++ b/chapter-modules/src/main/smp/sys/char.smp @@ -0,0 +1,8 @@ + +val toupper = { u8 c -> + return c >= 'a' && c <= 'z' ? c - ('a' - 'A') : c; +}; + +val tolower = { u8 c -> + return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; +}; diff --git a/chapter-modules/src/main/smp/sys/io.smp b/chapter-modules/src/main/smp/sys/io.smp new file mode 100644 index 00000000..d6263ffe --- /dev/null +++ b/chapter-modules/src/main/smp/sys/io.smp @@ -0,0 +1,64 @@ + +// Simple I/O Library + +// Write byte array to stdout +val p = { u8[~] str -> write(1,str); }; +val p_noInline = { u8[~] str -> write(1,str); }; + +// Return fd contents as a u8[]. +val read = { i32 fd -> + var buf = new u8[8*1024]; + i64 ptr = buf; + int len = 0; + while( true ) { + i32 rez = libc.read(fd,ptr,buf#); + if( rez > buf# ) libc._exit(libc.errno()); + len += rez; + if( rez < buf# ) + break; + libc._exit(-2); // Need to implement for more than 8K input + } + var out = new u8[len]; + ary.copy(out,0,buf,0,len); +}; + +// Write all of str to fd. +val write = { i32 fd, u8[~] str -> + i64 ptr = str; // Convert array pointer to a i64 for the native call + libc.write(fd,ptr,str#); +}; + +// Write all of str to fd. +val writeOff = { i32 fd, u8[~] str, u32 off, u32 len -> + i64 ptr = str; // Convert array pointer to a i64 for the native call + ptr += off; // Skip offset + // Cap at available length + if( len > str# - off ) len = str# - off; + // Do the write + libc.write(fd,ptr,len); +}; + +// Return stdin as a u8[]. +val stdin = { -> read(0); }; + +// Create file for writing. Return fd, or -1 and errno has the error code +val create = { u8[~] str -> + i64 ptr = sys.aryu8.str0(str); + i32 perms = 6*64 + 6*8 + 6; // 0666 + return libc.creat(ptr, perms); +}; + +// Open file for reading. Return fd, or -1 and errno has the error code +val open = { u8[~] str -> + i64 ptr = sys.aryu8.str0(str); + return libc.open(ptr, sys.libc.O_RDONLY, 0); +}; + +// Close an open fd. Return 0, or -1 and errno has the error code +val close = { i32 fd -> return libc.close(fd); }; + +// Remove a file. Return 0, or -1 and errno has the error code +val remove = { u8[~] str -> + i64 ptr = sys.aryu8.str0(str); + return libc.unlink(ptr); +}; diff --git a/chapter-modules/src/main/smp/sys/libc.smp b/chapter-modules/src/main/smp/sys/libc.smp new file mode 100644 index 00000000..17ee65cb --- /dev/null +++ b/chapter-modules/src/main/smp/sys/libc.smp @@ -0,0 +1,44 @@ + +// libc bindings: +// https://www.man7.org/linux/man-pages/man2/<libc call>.2.html + +// The buffers are int-versions of zero-terminated strings. +// int buf = aryu8.str0("some_file"); +// buf mode perm-> fd +{ i64 i32 i32 -> i32 } open = "C"; +// buf perm -> fd +{ i64 i32 -> i32 } creat = "C"; + +// fd buf len -> len +{ i32 i64 u32 -> i32 } write = "C"; +// fd buf len -> len +{ i32 i64 u32 -> i32 } read = "C"; +// fd -> err +{ i32 -> i32 } close = "C"; +// path -> err +{ i64 -> i32 } unlink = "C"; + +i32 O_RDONLY = 0; +i32 O_WRONLY = 1; +i32 O_RDWR = 2; +i32 O_CREAT = 512; // 02000 on x86_64-pc-cygwin +i32 O_TRUNC = 1024; // 04000 on x86_64-pc-cygwin + +i32 STDIN = 0; +i32 STDOUT = 1; +i32 STDERR = 2; + +// status +{ i32 -> i32 } _exit = "C"; +// errno is not an ordinary C data symbol; read it through the native runtime. +{ i32 } simple_errno = "C"; +val errno = { -> simple_errno(); }; +// addr len prot flags fd off -> void* +{ i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C"; +// mmap flags +i32 PROT_EXEC = 4; +i32 PROT_READ = 1; +i32 PROT_WRITE= 2; +i32 PROT_NONE = 0; +i32 MAP_PRIVATE = 2; +i32 MAP_ANON = 32; diff --git a/chapter-modules/src/main/smp/sys/scan.smp b/chapter-modules/src/main/smp/sys/scan.smp new file mode 100644 index 00000000..44bedaaa --- /dev/null +++ b/chapter-modules/src/main/smp/sys/scan.smp @@ -0,0 +1,39 @@ + +// Scanner; a simple string parser. +struct scan { + int !x; // Cursor + bool !bad; // True after a malformed token + u8[~] buf; // Buffer to parse + + new scan = { u8[~] b -> buf = b; }; + + // Skip whitespace + val skip = { -> + while( x < buf# && buf[x] <= ' ' ) + x++; + return self; + }; + + // True if find required character, and skip it; else False. + val match = { u8 c -> + skip(); + if( x >= buf# ) return false; + if( buf[x] != c ) return false; + x++; + return true; + }; + + // Parse a simple number + val number = { -> + skip(); + bool neg = match('-'); + if( x >= buf# || buf[x] < '0' || '9' < buf[x] ) { + bad = true; + return 0; + } + i64 n = 0; + while( x < buf# && '0' <= buf[x] & buf[x] <= '9' ) + n = n*10 + (buf[x++] - '0'); + return neg ? -n : n; + }; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/BoundaryTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/BoundaryTest.java new file mode 100644 index 00000000..68d8175f --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/BoundaryTest.java @@ -0,0 +1,91 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import org.junit.Test; +import static org.junit.Assert.*; + +// The new module boundary must preserve the preceding chapters' examples. +public class BoundaryTest { + @Test public void testLocalConstruction() throws java.io.IOException { + String src="struct Counter { int x; int y; int z; new Counter={int n -> while(x<n) { int old=x; y=x+1; x=y+1; z=old; }}; }; val c=new Counter(arg); return c.x*100+c.z;"; + CodeGen code=new CodeGen(src).driver(CodeGen.Phase.LocalSched); + assertEquals("2018",Eval2.eval(code,19)); + assertEquals("0",Eval2.eval(code,0)); + code._stop.walk(n -> { + if( n instanceof StoreNode st ) assertEquals(0,st.cfg0().loopDepth()); + return null; + }); + code=new CodeGen("struct Byte { new Byte={int n -> self.x=n; self.x+=3;}; u8 x; }; return new Byte(255).x;").driver(CodeGen.Phase.TypeCheck); + assertEquals("2",Eval2.eval(code,0)); + // data is still unset when the constructor reads and updates x. + code=new CodeGen("struct Box { int x=7; int[] data; new Box={ -> self.x++; int n=self.x; data=new int[n]; }; }; return new Box().data#;") + .driver(CodeGen.Phase.TypeCheck); + assertEquals("8",Eval2.eval(code,0)); + for( String field : new String[]{"data","self.data"} ) { + try { + new CodeGen("struct Box { int[] data; new Box={ -> int n="+field+"#; data=new int[n]; }; }; return new Box();") + .driver(CodeGen.Phase.TypeCheck); + fail("An uninitialized field must not be read"); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("might not be initialized")); + } + } + java.nio.file.Path cfile=java.nio.file.Path.of("build/objs/constructionBoundary.c"); + java.nio.file.Files.createDirectories(cfile.getParent()); + java.nio.file.Files.writeString(cfile,""" + __attribute__((CALL_CONV)) extern long long count(long long); + int main(void) { return count(19)!=2018 || count(0)!=0; } + """); + TestC.run("val count={int arg ->"+src+"};","constructionBoundary",null,TestC.CALL_CONVENTION, + TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi",cfile.toString(),"",-1); + } + + @Test public void testRecursiveCallback() { + String src=""" + val run={int n -> + val power={int base,int exponent -> if(exponent<=0) return 1; return base*power(base,exponent-1);}; + val sumPowers={int n,{int int -> int} f -> if(n<=0) return 0; return f(2,n)+sumPowers(n-1,f);}; + return sumPowers(n,power); + }; + return run(arg); + """; + for( long seed : new long[]{2,13,123} ) { + CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.Opto); + ParmNode base=code._start.walk(n -> n instanceof ParmNode p && p._label.equals("base") ? p : null); + assertNotNull(base); + assertSame(TypeInteger.constant(2),base._type); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("14",Eval2.eval(code,3)); + } + } + + @Test public void testNativeExposure() { + for( String tail : new String[]{ + "retain(power); return power(2,n);", + "if(n) { retain(power); while(true) {} } return power(2,n);" + } ) { + String src="val run={int n -> val power={int base,int exponent -> if(exponent<=0) return 1; return base*power(base,exponent-1);}; { {int int -> int} -> int } retain=\"C\";"+tail+"}; return run(arg);"; + CodeGen code=new CodeGen(src).driver(CodeGen.Phase.Opto); + ParmNode base=code._start.walk(n -> n instanceof ParmNode p && p._label.equals("base") ? p : null); + assertNotNull(base); + assertSame(TypeInteger.BOT,base._type); + code.driver(CodeGen.Phase.TypeCheck); + } + } + + @Test public void testLibrarySource() { + CodeGen code=new CodeGen("return sys.char.toupper('a');").driver(CodeGen.Phase.TypeCheck); + assertEquals("65",Eval2.eval(code,0)); + } + + @Test public void testLazyTables() { + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen("src/test/smp/lazy","build/objs","App/Main",null,seed,true,TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); + assertEquals("0",Eval2.eval(code,0)); + assertEquals("125",Eval2.eval(code,1)); + } + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/BrainFuckTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/BrainFuckTest.java new file mode 100644 index 00000000..c2713e4f --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/BrainFuckTest.java @@ -0,0 +1,87 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.isa.eval.EvalRisc5; + +import com.seaofnodes.isa.eval.EvalArm64; + +import java.io.IOException; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class BrainFuckTest { + + @Test public void testBrainFuck() throws IOException { + String brain_fuck = "Hello World!\n"; + String src = +""" +// -*- mode: java; -*- +val brain_fuck = { -> + var !program = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."; + 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 == '>') { + d++; + } else if (command == '<') { + d--; + } else if (command == '+') { + data[d]++; + } else if (command == '-') { + data[d]--; + } else if (command == '.') { + // 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 == ',') { + data[d] = 42; + } else if (command == '[') { + if (data[d] == 0) { + for( int d = 1; d > 0; ) { + command = program[++pc]; + if (command == 91) d++; + if (command == 93) d--; + } + } + } else if (command == ']') { + if (data[d]) { + for( int d = 1; d > 0; ) { + command = program[--pc]; + if (command == 93) d++; + if (command == 91) d--; + } + } + } + } + return output; +}; +"""; + TestC.runC(src,"brain_fuck", brain_fuck, 40); + + EvalRisc5 R5 = TestRisc5.build( src, "brain_fuck", 0, 25, false); + int trap = R5.step(100000); + assertEquals(0,trap); + int ptr = (int)R5.regs[com.seaofnodes.simple.node.cpus.riscv.riscv.A0]; + assertEquals(brain_fuck.length(),R5.ld4s(ptr)); + for( int i=0; i<brain_fuck.length(); i++ ) + assertEquals(brain_fuck.charAt(i), R5.ld1z(ptr+4+i)); + + EvalArm64 A5 = TestArm64.build("brain_fuck", src, 0, 28, false); + int trap_arm = A5.step(100000); + assertEquals(0,trap_arm); + assertEquals(0,trap); + int ptr_arm = (int)A5.regs[com.seaofnodes.simple.node.cpus.arm.arm.X0]; + assertEquals(brain_fuck.length(),A5.ld4s(ptr_arm)); + + for( int i=0; i<brain_fuck.length(); i++ ) + assertEquals(brain_fuck.charAt(i), A5.ld1z(ptr_arm+4+i)); + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java new file mode 100644 index 00000000..31c0302b --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java @@ -0,0 +1,40 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMem; +import org.junit.Test; +import static org.junit.Assert.*; + +public class BulkMemPhiTest { + // Reduced from Capitalize's Lower-phase failure. A precise MemMerge slot + // may still be supplied by bulk memory which covers the requested alias. + @Test public void testBulkInPreciseSlot() { + CodeGen code=new CodeGen(""); + code._phase=CodeGen.Phase.Lower; + Node m0=code.con(TypeMem.START), m1=code.con(TypeMem.BOT); + Node ctrl=new CProjNode(code._start,0,"Ctrl").init(); + Node iff=new IfNode(ctrl,code.con(TypeInteger.BOOL)).init(); + RegionNode region=new RegionNode(null,null, + new CProjNode(iff,0,"True").init(), + new CProjNode(iff,1,"False").init()).init(); + BulkMemPhiNode bulk=new BulkMemPhiNode("$mem",region,m0,m1).init(); + // A later init check proves its call arm dead. Its precise Phi can + // collapse before the surrounding aggregate gets another peephole. + RegionNode done=new RegionNode(null,null,region,code.XCTRL).init(); + MemPhiNode precise=new MemPhiNode("$4",4,done,bulk,m1).init(); + MemMergeNode merge=new MemMergeNode(null,m0); + merge.alias(4,precise); + merge.init(); + Node folded=precise.peepholeOpt(); + assertSame(bulk,folded); + precise.subsume(folded); + + assertFalse(bulk.isSplit(4)); + assertSame(bulk,merge.alias(4)); + // Splitting this still-covered alias must tolerate the direct bulk use. + Node mem = bulk.idealize(); + bulk.subsume(mem); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter01Test.java new file mode 100644 index 00000000..fa7fb68a --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter01Test.java @@ -0,0 +1,95 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +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("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(); + Node use = code.expr(); + 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().print()); + } + + @Test + public void testBad4() { + try { + new CodeGen("return 100").parse(); + fail(); + } catch( RuntimeException e ) { + assertEquals("Syntax error, expected `;` but found `EOF`",e.getMessage()); + } + } + + @Test + public void testNotBad5() { + // this test used to fail in chapter 1 + assertEquals("return -100;", new CodeGen("return -100;").parse().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 but found `}`",e.getMessage()); + } + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter02Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter02Test.java new file mode 100644 index 00000000..a51ac202 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter02Test.java @@ -0,0 +1,61 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +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(); + 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/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter03Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter03Test.java new file mode 100644 index 00000000..f1e4af12 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter03Test.java @@ -0,0 +1,66 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +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 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 `}` but found `EOF`",e.getMessage()); + } + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter04Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter04Test.java new file mode 100644 index 00000000..1132375e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter04Test.java @@ -0,0 +1,159 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +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 testSub0() { + CodeGen code = new CodeGen("return arg-0;"); + code.parse(); + assertEquals("return arg;", code.print()); + } + + @Test + public void test0Sub() { + CodeGen code = new CodeGen("return 0-arg;"); + code.parse(); + assertEquals("return (-arg);", code.print()); + } + + @Test + public void testSubNeg() { + CodeGen code = new CodeGen("return 3--arg;"); + code.parse(); + assertEquals("return (arg+3);", code.print()); + } + + @Test + public void testNegSub() { + CodeGen code = new CodeGen("return -arg-3;"); + code.parse(); + assertEquals("return (-(arg+3));", 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)).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/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter05Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter05Test.java new file mode 100644 index 00000000..afdb1071 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter05Test.java @@ -0,0 +1,204 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,3,5,2,4);", 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.driver(CodeGen.Phase.FinalizeIR); + String ir = code.print(); + assertTrue(ir, + ir.equals("return ((arg+Phi(Region,1,0))+Phi(Region,2,0));") || + ir.equals("return (Phi(Region,((!=0)arg+1),0)+Phi(Region,2,0));")); + } + + @Test + public void testMerge5() { + CodeGen code = new CodeGen( +""" +int a=arg==2; +if( arg==1 ) +{ + a=arg==3; +} +return a; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return (arg==Phi(Region,3,2));", code.print()); + } + + @Test + public void testMergeSameCon() { + CodeGen code = new CodeGen( +""" +int a=arg; +if( arg==1 ) + a=3; +else + a=3; +return a; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 3;", 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` but found `;`",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/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter06Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter06Test.java new file mode 100644 index 00000000..d1af5d14 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter06Test.java @@ -0,0 +1,305 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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)).driver(CodeGen.Phase.FinalizeIR); + 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)).driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,3,5,4);", 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)); + code.driver(CodeGen.Phase.FinalizeIR); + 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)); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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)); + code.driver(CodeGen.Phase.FinalizeIR); + 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)); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,6,1,5);", 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)); + code.driver(CodeGen.Phase.FinalizeIR); + 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)); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 5;", code.print()); + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter07Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter07Test.java new file mode 100644 index 00000000..7094c0d5 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter07Test.java @@ -0,0 +1,222 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print()); + assertTrue(code.ctrl() instanceof CProjNode); + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter08Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter08Test.java new file mode 100644 index 00000000..376e472b --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter08Test.java @@ -0,0 +1,208 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,(Phi(Loop,arg,Add)+1),Add,Phi_arg);", 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Loop,1,Phi(Region,Phi_a,(Phi_a+1),Phi_a));", 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return (Phi(Loop,1,Add)+1);", code.print()); + assertTrue(code.ctrl() instanceof CProjNode prj && prj._idx==1 ); + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter09Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter09Test.java new file mode 100644 index 00000000..7025b98a --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter09Test.java @@ -0,0 +1,299 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +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.driver(CodeGen.Phase.FinalizeIR); + } + + @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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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; +} +return 0; + """); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + } + + @Test + public void testWhile0() { + CodeGen code = new CodeGen("while(0) continue; if(0) arg=0; return arg;"); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return arg;", code.print()); + } + + @Test + public void testWhile1() { + CodeGen code = new CodeGen(""" +if(0) while(0) { + int arg=arg; + while(0) {} +} +return 0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + } + + + @Test + public void testPrecedence() { + CodeGen code = new CodeGen("return 3-1+2;"); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 4;", code.print()); + } + + @Test + public void testSwap2() { + CodeGen code = new CodeGen("return 1+(1+1);"); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Top;", code.print()); + } + + + @Test + public void testFuzz2() { + CodeGen code = new CodeGen("return 0+-0;"); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + } + + @Test + public void testFuzz3() { + CodeGen code = new CodeGen("int v0=0; while(0==69) while(v0) return 0; return 1;"); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 1;", 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Top;", code.print()); + } + + @Test + public void testFuzz6() { + CodeGen code = new CodeGen( +""" +int v0=0; +while(0==1) while(v0) + v0=1+v0; +return 0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Top;", code.print()); + } + + @Test + public void testFuzz8() { + CodeGen code = new CodeGen("while(arg) arg = arg - 1; return arg;"); + code.driver(CodeGen.Phase.FinalizeIR); + 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/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter10Test.java new file mode 100644 index 00000000..39c7040e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter10Test.java @@ -0,0 +1,508 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter10Test { + @Test public void testConditionalStoreSharing() { + // Sharing the writes on two of three paths must not write on the third. + // Return the object so Load forwarding cannot hide the physical Store. + String src = """ + struct S { int x; }; + !S !a = new S; + a.x = 10; + if (arg == 1) a.x = 9; + else if (arg == 2) a.x = 9; + return a; + """; + var code = new CodeGen(src).driver(CodeGen.Phase.LocalSched); + for( int arg : new int[]{0,1,2,3} ) { + long expected = (arg==1 || arg==2) ? 9 : 10; + assertEquals("Test.S{x="+expected+"}",Eval2.eval(code,arg)); + } + } + + @Test public void testDeadReferenceReturn() { + for( String body : new String[] { + "return new S; return 0;", + "if(1) return new S; else return 0;" + } ) { + String src = "struct S { int x; }; " + body; + var code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + // This chapter also has constructor/class-init Returns in the CompUnit. + var entry = (com.seaofnodes.simple.node.FunNode)code._start.uctrl(); + org.junit.Assert.assertTrue(body, + entry.ret().expr()._type instanceof com.seaofnodes.simple.type.TypeMemPtr); + } + } + + + private static final String NULLABLE_POINT_SOURCE = """ + struct Point { int x; new Point = { int v -> x = v; }; }; + !Point?[] !points = new !Point?[2]; + points[arg] = new Point(42); + Point? p = points[1]; + """; + + @Test + public void testNullGuards() { + // Issue #246: != null lowers to !!p. Follow every negation, on either arm. + for( String body : new String[] { + "if (p != null) return p.x; return -1;", + "if (null != p) return p.x; return -1;", + "if (!!!!p) return p.x; return -1;", + "if (!!!p) return -1; return p.x;", + "val b = !!p; if (b) return p.x + b - 1; return -1;", + "if (!(p == null || arg == 0)) return p.x; return -1;" + } ) + for( long seed : new long[] { 1, 42, 126 } ) { + CodeGen code = new CodeGen(NULLABLE_POINT_SOURCE+body,seed,true) + .driver(CodeGen.Phase.TypeCheck); + assertEquals(body,"-1",Eval2.eval(code,0)); + assertEquals(body,"42",Eval2.eval(code,1)); + code.driver(CodeGen.Phase.LocalSched); + } + } + + @Test + public void testShortCircuitGuardScheduling() { + // The call result exists only on the RHS path, not above the merge. + CodeGen code = new CodeGen(""" + val f = { int n -> (n+1)&7; }; + int x = 0; + if (!!(arg && (x=f(arg)))) return x; + return -1; + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("-1",Eval2.eval(code,0)); + assertEquals("2",Eval2.eval(code,1)); + assertEquals("-1",Eval2.eval(code,7)); + code.driver(CodeGen.Phase.LocalSched); + } + + @Test + public void testNullGuardErrors() { + for( String body : new String[] { + "return p.x;", + "if (p == null) return p.x; return -1;", + "if (!!points) return p.x; return -1;", + "if (!!p) { int x = p.x; } return p.x;" + } ) { + try { + new CodeGen(NULLABLE_POINT_SOURCE+body).driver(CodeGen.Phase.TypeCheck); + fail(body); + } catch( Parser.ParseException e ) { + assertEquals(body,"Might be null accessing 'x'",e.getMessage()); + } + } + } + + @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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("Stop[ return Test.Vector2D; return MEM[ 2:___ 3:.x=0; 4:.y=0;]; return Test.Vector2D; ]", code.print()); + } + + @Test + public void testBug() { + CodeGen code = new CodeGen(""" +struct s0 { + int v0; +}; +s0? v1=null; +int v3=v1.zAicm; +"""); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Might be null accessing 'zAicm'",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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("Stop[ return Phi(Loop,0,(Phi_a+2)); return MEM[ 2:___ 3:.a=0;]; return Test.Bar; ]", 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.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Type null is not of declared type *Test.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.driver(CodeGen.Phase.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.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Type null is not of declared type *Test.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.a; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,1,.a);", 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("Stop[ return Phi(Loop,0,(Phi(Loop,0,(Phi_x+1))+Phi_sum)); return MEM[ 2:___ 3:.x=0; 4:.len=0;]; return Test.Iter; ]", 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("Stop[ return Phi(Loop,Test.s0,Phi(Region,Test.s0,Phi_ret)); return MEM[ 2:___ 3:.v0=0;]; return Test.s0; ]", 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.v0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return .v0;", code.print()); + } + + + @Test + public void test3() { + CodeGen code = new CodeGen(""" +struct _s0 {int v0;}; +!_s0 !ret = new _s0; +while(arg < 10) { + !_s0 !v1 = new _s0; + if (arg == 5) ret=v1; + arg = arg + 1; +} +return ret.v0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return .v0;", code.print()); + } + + + @Test + public void testModGlobalInConstructor() { + CodeGen code = new CodeGen( +""" +int cnt = 1; +// cnt is incremented exactly once, it is not part of s0 constructor +// hence s0.x is always 1, and cnt remains at 2 +struct s0 { int x = cnt++; }; +s0 a = new s0(); +s0 b = new s0(); +// a.x==1, b.x==1, cnt==2 +return a.x * 100 + b.x * 10 + cnt; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("Stop[ return 112; return MEM[ 2:___ 3:___ 4:.x=1;]; return Test.s0; ]", 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("Syntax error, expected `;` but found `.`",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; + } +} +return 0; + """); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.TypeCheck); + assertEquals("Stop[ return Test.s0; return MEM[ 2:___ 3:.f0=0;]; return Test.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.driver(CodeGen.Phase.FinalizeIR); + } + + @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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("Stop[ return (const)Test.s0; return MEM[ 2:___ 3:.f0=0;]; return Test.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; + } + } + } + } + } } +} +return 0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + } + + @Test + public void testBug9() { + CodeGen code = new CodeGen(""" +int v0=arg==0; +while(v0) continue; +return 0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter12Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter12Test.java new file mode 100644 index 00000000..fae7611d --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter12Test.java @@ -0,0 +1,283 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter12Test { + @Ignore + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 3; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 3;", code.print()); + assertEquals("3", 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.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Might be null accessing 'next'",e.getMessage()); } + } + + @Test + public void testLinkedList1() { + CodeGen code = new CodeGen( +""" +struct _LLI { !_LLI? !next; int q; }; +!_LLI? !head = null; +while( arg ) { + !_LLI !x = new _LLI; + x.next = head; + x.q = arg; + head = x; + arg = arg-1; +} +if( !head ) return 0; +_LLI? next = head.next; +if( next==null ) return 1; +return next.q; +"""); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("return Phi(Region,1,.q,0);", code.print()); + assertEquals("2", Eval2.eval(code, 3)); + } + + @Test + public void testCoRecur() { + CodeGen code = new CodeGen( +""" +struct _int0 { int i; !Test._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.driver(CodeGen.Phase.TypeCheck); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return null;", code.print()); + } + + @Test + public void testNullRef1() { + CodeGen code = new CodeGen( +""" +struct M { int m; }; +struct N { !M !next; int i; new N = { !M m -> next = m; }; }; +N n = new N(new M); +return n.next; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("Stop[ return (const)Test.M; return MEM[ 2:___ 3:.m=0;]; return Test.M; return MEM[ 2:___ 3:___ 4:___ 5:___ 6:___ 7:.next=Parm_m(Test.N.N,*Test.M {i64 !m; }); 8:.i=0;]; return MEM[ 2:___ 3:___ 4:___ 5:___ 6:___ 7:___ 8:.i=0;]; return Test.N; ]", code.print()); + } + + @Test + public void testNullRef2() { + CodeGen code = new CodeGen( +""" +struct M { int m; }; +struct N { !M !next; int i; new N = { !M m -> next = m; }; }; +N n = new N(null); +return n.next; +"""); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Argument #0 isa null, but must be a *Test.M {i64 !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; +"""); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("3",Eval2.eval(code,0)); + } + + @Test + public void testNullRef4() { + CodeGen code = new CodeGen("return -null-5/null-5;"); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Cannot null + 5",e.getMessage()); } + } + + @Test public void testNullRef5() { + CodeGen code = new CodeGen("return null+42;"); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Cannot null + 42",e.getMessage()); } + } + + @Test + public void testEmpty() { + CodeGen code = new CodeGen( +""" +struct _S{}; +return 0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.TypeCheck); 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Test._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.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Might be null accessing 'i'",e.getMessage()); } + } + + @Test + public void testCoRecur2() { + CodeGen code = new CodeGen( +""" +struct _A { !Test._B? !f0; !Test._C? !f1; }; !_A !a = new _A; +struct _B { !Test._C? !f0; !Test._A? !f1; }; !_B !b = new _B; +struct _C { !Test._A? !f0; !Test._B? !f1; }; !_C !c = new _C; + +a.f0=b; a.f1=c; +b.f0=c; b.f1=a; +c.f0=a; c.f1=b; + +return a.f0.f1.f0.f1.f0; + +"""); + code.driver(CodeGen.Phase.FinalizeIR).driver(CodeGen.Phase.Schedule).localSched(); + assertEquals("return Test._B;", code.print()); + assertEquals("Test._B{f0=Test._C{f0=Test._A{f0=$cyclic,f1=$cyclic},f1=$cyclic},f1=$cyclic}", Eval2.eval(code, 0)); + } + + @Test + public void testCoRecur3() { + CodeGen code = new CodeGen( +""" +struct _A { !Test._L? !a; !Test._T? !b; !Test._F? !c; }; !_A !a = new _A; +struct _B { !Test._M? !a; !Test._U? !b; !Test._G? !c; }; !_B !b = new _B; +struct _C { !Test._N? !a; !Test._V? !b; !Test._H? !c; }; !_C !c = new _C; +struct _D { !Test._O? !a; !Test._W? !b; !Test._I? !c; }; !_D !d = new _D; +struct _E { !Test._P? !a; !Test._X? !b; !Test._J? !c; }; !_E !e = new _E; +struct _F { !Test._Q? !a; !Test._Y? !b; !Test._K? !c; }; !_F !f = new _F; +struct _G { !Test._R? !a; !Test._Z? !b; !Test._L? !c; }; !_G !g = new _G; +struct _H { !Test._S? !a; !Test._A? !b; !Test._M? !c; }; !_H !h = new _H; +struct _I { !Test._T? !a; !Test._B? !b; !Test._N? !c; }; !_I !i = new _I; +struct _J { !Test._U? !a; !Test._C? !b; !Test._O? !c; }; !_J !j = new _J; +struct _K { !Test._V? !a; !Test._D? !b; !Test._P? !c; }; !_K !k = new _K; +struct _L { !Test._W? !a; !Test._E? !b; !Test._Q? !c; }; !_L !l = new _L; +struct _M { !Test._X? !a; !Test._F? !b; !Test._R? !c; }; !_M !m = new _M; +struct _N { !Test._Y? !a; !Test._G? !b; !Test._S? !c; }; !_N !n = new _N; +struct _O { !Test._Z? !a; !Test._H? !b; !Test._T? !c; }; !_O !o = new _O; +struct _P { !Test._A? !a; !Test._I? !b; !Test._U? !c; }; !_P !p = new _P; +struct _Q { !Test._B? !a; !Test._J? !b; !Test._V? !c; }; !_Q !q = new _Q; +struct _R { !Test._C? !a; !Test._K? !b; !Test._W? !c; }; !_R !r = new _R; +struct _S { !Test._D? !a; !Test._L? !b; !Test._X? !c; }; !_S !s = new _S; +struct _T { !Test._E? !a; !Test._M? !b; !Test._Y? !c; }; !_T !t = new _T; +struct _U { !Test._F? !a; !Test._N? !b; !Test._Z? !c; }; !_U !u = new _U; +struct _V { !Test._G? !a; !Test._O? !b; !Test._A? !c; }; !_V !v = new _V; +struct _W { !Test._H? !a; !Test._P? !b; !Test._B? !c; }; !_W !w = new _W; +struct _X { !Test._I? !a; !Test._Q? !b; !Test._C? !c; }; !_X !x = new _X; +struct _Y { !Test._J? !a; !Test._R? !b; !Test._D? !c; }; !_Y !y = new _Y; +struct _Z { !Test._K? !a; !Test._S? !b; !Test._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.driver(CodeGen.Phase.FinalizeIR).driver(CodeGen.Phase.Schedule).localSched(); + assertEquals("return Test._R;", code.print()); + assertEquals("Test._R{a=Test._C{a=Test._N{a=Test._Y{a=Test._J{a=Test._U{a=Test._F{a=Test._Q{a=Test._B{a=Test._M{a=Test._X{a=Test._I{a=Test._T{a=Test._E{a=Test._P{a=Test._A{a=Test._L{a=Test._W{a=Test._H{a=Test._S{a=Test._D{a=Test._O{a=Test._Z{a=Test._K{a=Test._V{a=Test._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/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter13Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter13Test.java new file mode 100644 index 00000000..214bc69e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter13Test.java @@ -0,0 +1,284 @@ +package com.seaofnodes.simple; +import com.seaofnodes.simple.codegen.CodeGen; + + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class Chapter13Test { + + @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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,0,Phi(Loop,1,Phi(Region,Phi_primeCount,Phi_primeCount,(Phi_primeCount+1))));", 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 testClassField() { + CodeGen code = new CodeGen( +""" +if( arg < 2 ) return 0; +int primeCount = 1; +while( arg-- ) + primeCount++; +return primeCount; +"""); + code.driver(CodeGen.Phase.LocalSched); + assertEquals("0", Eval2.eval(code, 1)); // No primes 1 or below + assertEquals("3", Eval2.eval(code, 2)); // +} + + + @Test + public void testAntiDeps1() { + CodeGen code = new CodeGen( +""" +struct _S { int f; }; +!_S !v=new _S; +v.f = 2; +_S q=new _S; +int i=q.f; +i=v.f; +if (arg) v.f=1; +return i; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.f; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,.f,2);", 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.driver(CodeGen.Phase.FinalizeIR); + 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.f; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Loop,0,2);", code.print()); + for( int arg : new int[]{-1,0,1,3,100} ) + assertEquals("0",Eval2.eval(code,arg)); + } + + @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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.f; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", 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.driver(CodeGen.Phase.FinalizeIR); + //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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java new file mode 100644 index 00000000..e058a822 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java @@ -0,0 +1,104 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class Chapter14FloatTest { + @Test public void testNumericAssignments() { + String[][] tests = { + {"int rounded=17.3/2.5; return rounded;", "6", "6"}, + {"int rounded=arg/2.5; return rounded;", "6", "-6"}, + {"int rounded=0; rounded=arg/2.5; return rounded;", "6", "-6"}, + {"i8 rounded=arg*16.0; return rounded;", "16", "-16"}, + {"int rounded=1e100; return rounded;", "9223372036854775807", "9223372036854775807"}, + {"int rounded=-1e100; return rounded;", "-9223372036854775808", "-9223372036854775808"}, + {"int rounded=1e300*1e300-1e300*1e300; return rounded;", "0", "0"}, + {"f32 rounded=arg+16777216.0; return rounded;", "1.6777232E7", "1.6777199E7"} + }; + for( String[] test : tests ) { + var code = new CodeGen(test[0]).driver(CodeGen.Phase.TypeCheck); + assertEquals(test[0],test[1],Eval2.eval(code,17)); + assertEquals(test[0],test[2],Eval2.eval(code,-17)); + } + } + + + @Test public void testSubZeroFloat() { + var code = new CodeGen("flt x = arg; return 0-x;").driver(CodeGen.Phase.TypeCheck); + // Compare strings so +0.0 and -0.0 remain distinct. + assertEquals("0.0", Eval2.eval(code,0)); + assertEquals("-1.0", Eval2.eval(code,1)); + } + + @Test public void testDeadNumericReturns() { + for( String[] test : new String[][] { + {"if(1) return 7; else return 2.5;", "7"}, + {"if(0) return 7; else return 2.5;", "2.5"}, + {"return 7; return 2.5;", "7"}, + {"return 2.5; return 7;", "2.5"} + } ) { + String src = test[0]; + var code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals(src,test[1],Eval2.eval(code,0)); + } + } + + + @Ignore + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + + @Test + public void testFloat() { + CodeGen code = new CodeGen( +""" +flt x = 3.14; +return 0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + assertEquals("0", 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; +"""); + // TODO: One of the problems with <clinit> returning an OS error code, + // is it chops all results to int. + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Loop,(flt)arg,((Phi_guess+(ToFloat/Phi_guess))*0.5f));", 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return ((flt)arg==(ToFloat+1.0f));", code.print()); + assertEquals("0", Eval2.eval(code, 1)); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter14Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter14Test.java new file mode 100644 index 00000000..bc2ad8d3 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter14Test.java @@ -0,0 +1,228 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +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 Chapter14Test { + + @Ignore + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 3.14; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.TypeCheck); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter15Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter15Test.java new file mode 100644 index 00000000..c2071cb6 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter15Test.java @@ -0,0 +1,412 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import org.junit.Ignore; +import static org.junit.Assert.*; + +public class Chapter15Test { + + @Test public void testArrayLoadDominatingControl() { + String src = """ + int[] !a=new int[arg+1]; a[arg]=7; + if(a#>1) { + if(arg==2) a[1]=9; + return a[1]; + } + return 42; + """; + var code = new CodeGen(src).parse(); + var stop = code._stop; + var load = stop.walk(n -> n instanceof com.seaofnodes.simple.node.LoadNode ld && ld._name.equals("[]") ? ld : null); + org.junit.Assert.assertNotNull(load); + // The parser uses current control as a placeholder for a range check. + // Model an explicit check already performed before the inner diamond: + // both predecessors are below the a#>1 true projection. + var region = (com.seaofnodes.simple.node.RegionNode)load.in(0); + var checked = region.idom().cfg(0); + org.junit.Assert.assertTrue(checked instanceof com.seaofnodes.simple.node.CProjNode); + load.setDef(0,checked); + code.driver(CodeGen.Phase.TypeCheck); + org.junit.Assert.assertTrue("Common dominating control permits Phi splitting",load.isDead()); + var split = stop.walk(n -> n instanceof com.seaofnodes.simple.node.LoadNode ld && ld._name.equals("[]") ? ld : null); + org.junit.Assert.assertNotNull(split); + org.junit.Assert.assertSame("The surviving read retains the earlier check",checked,split.in(0)); + code.driver(CodeGen.Phase.LocalSched); + assertEquals("42",Eval2.eval(code,0)); + assertEquals("7",Eval2.eval(code,1)); + assertEquals("9",Eval2.eval(code,2)); + } + + + @Test public void testArrayLoadControl() { + // With arg==0, a has length 1 and a[1] must not execute. The memory + // merge must not pull the read above its array-length guard. + String src = """ + int n=1; if(arg) n=2; + int[] !a=new int[n]; a[arg]=7; + if(arg) a[1]=9; + if(a#>1) return a[1]; + return 42; + """; + var code = new CodeGen(src).parse(); + var stop = code._stop; + var load = stop.walk(n -> n instanceof com.seaofnodes.simple.node.LoadNode ld && ld._name.equals("[]") ? ld : null); + org.junit.Assert.assertNotNull(load); + org.junit.Assert.assertNotNull("Element loads carry their range-check control",load.in(0)); + code.driver(CodeGen.Phase.LocalSched); + assertEquals("42",Eval2.eval(code,0)); + assertEquals("9",Eval2.eval(code,1)); + } + + + @Ignore + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 3.14; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Test._C;", code.print()); + assertEquals("Test._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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return []*Test._A?;", code.print()); + assertEquals("*Test._A {i64 !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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,1.2,.y,2.3);", 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.driver(CodeGen.Phase.TypeCheck); + assertEquals("return Phi(Region,1.2,1.2,.y);", 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.driver(CodeGen.Phase.TypeCheck); + assertEquals("return Test._Tree;", code.print()); + assertEquals("Test._Tree{_kids=*Test._Tree {*[]*Test._Tree?[mutable]? !_kids; }?[ Test._Tree{_kids=null},null]}", Eval2.eval(code, 0)); + } + + @Test + public void testNestedStructAddMemProj() { + CodeGen code = new CodeGen( +""" +struct _S { int a; int[] !b; new _S = { int[] ary -> b=ary; }; }; +return 0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.TypeCheck); + assertEquals("return (.[]+(.[]*1000));", code.print()); + assertEquals("1006", Eval2.eval(code, 4)); + } + + @Test @Ignore + public void sieveOEratosthenes() throws IOException { + String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/sieve.smp")); + CodeGen code = new CodeGen(src+"return sieve(arg);"); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return []u32;", code.print()); + assertEquals("u32[ 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); fail(); } + catch( Exception e ) { assertEquals("Cannot allocate a flt",e.getMessage()); } + } + + @Test + public void testBad1() { + CodeGen code = new CodeGen( +""" +flt is = new int[2]; +return is; +"""); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Type *[]i64 is not of declared type flt",e.getMessage()); } + } + + @Test + public void testBad2() { + CodeGen code = new CodeGen( +""" +int[] is = new int[3.14]; +return is[1]; +"""); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Cannot '<<' 3.14",e.getMessage()); } + } + + @Test + public void testBad3() { + CodeGen code = new CodeGen(""" +int[] is = new int[arg]; +return is[1]; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 4)); + try { Eval2.eval(code,-1); fail(); } + catch( NegativeArraySizeException e ) { assertEquals("-1",e.getMessage()); } + // This test will not pass (i.e., failed range-check) until we have range checks. + // Without the range check, the load `is[1]` optimizes against the `new int[]` + // and assumes it loads a zero then folds away, and the Eval2 never sees an OOB load. + //try { Eval2.eval(code,0); fail(); } + //catch( ArrayIndexOutOfBoundsException e ) { assertEquals("Array index 1 out of bounds for array length 0",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.driver(CodeGen.Phase.FinalizeIR); + 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)) {} +return 0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + 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; + } + } +} +return 0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter16Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter16Test.java new file mode 100644 index 00000000..2942cfee --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter16Test.java @@ -0,0 +1,148 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter16Test { + + @Ignore + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 3.14; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 5;", code.print()); + assertEquals("5", Eval2.eval(code, 0)); + } + + + @Test + public void testConstruct0() { + CodeGen code = new CodeGen(""" +struct _X { int x=3; }; +_X z = new _X; +return z.x; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 3;", code.print()); + assertEquals("3", Eval2.eval(code, 0)); + } + + @Test + public void testConstruct2() { + CodeGen code = new CodeGen(""" +struct _X { int x=3; new _X = { int xx -> x=xx; }; }; +_X z = new _X(4); +return z.x; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 4;", code.print()); + assertEquals("4", Eval2.eval(code, 0)); + } + + + @Test + public void testLinkedList1() { + CodeGen code = new CodeGen( +""" +struct _LLI { !_LLI? !next; int i; new _LLI = { !_LLI? !n, int ii -> next=n; i=ii; }; }; +!_LLI? !head = null; +while( arg ) { + head = new _LLI(head,arg); + arg = arg-1; +} +if( !head ) return 0; +_LLI? next = head.next; +if( !next ) return 1; +return next.i; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,1,.i,0);", 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; new _LLI = { !_LLI? !n, int a -> + next=n; + int !tmp=a; + while( a > 10 ) { + tmp = tmp + a; + a = a - 1; + } + i=tmp; +}; }; +!_LLI? !head = null; +while( arg ) { + head = new _LLI(head,arg); + arg = arg-1; +} +if( !head ) return 0; +_LLI? next = head.next; +if( !next ) return 1; +return next.i; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,1,.i,0);", 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Test._Square;", code.print()); + assertEquals("Test._Square{side=3.0,diag=1.7320508075688772}", Eval2.eval(code, 3)); + assertEquals("Test._Square{side=4.0,diag=2.0}", Eval2.eval(code, 4)); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter17aTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter17aTest.java new file mode 100644 index 00000000..ea7afe56 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter17aTest.java @@ -0,0 +1,109 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter17aTest { + + @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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,9,3);", code.print()); + assertEquals("3", Eval2.eval(code, 0)); + assertEquals("9", Eval2.eval(code, 1)); + } + + @Test + public void testConstruct1() { + CodeGen code = new CodeGen(""" +struct _X { int ~x; new _X = { int xx -> x=xx; }; }; +_X z = new _X(3); +return z.x; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 3;", code.print()); + assertEquals("3", Eval2.eval(code, 0)); + } + + @Test + public void testStructFinal0() { + CodeGen code = new CodeGen(""" +struct _Point { int ~x, ~y; new _Point = { int xx, int yy -> x=xx; y=yy; }; }; +_Point p = new _Point(3,4); +return p; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return (const)Test._Point;", code.print()); + assertEquals("Test._Point{x=3,y=4}", Eval2.eval(code, 0)); + } + + @Test + public void testStructFinal1() { + CodeGen code = new CodeGen(""" +struct _Point { int x=3, y=4; new _Point = { int xx, int yy -> x=xx; y=yy; }; }; +~_Point p = new _Point(5,6); +p.x++; +return p; +"""); + try { code.driver(CodeGen.Phase.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.driver(CodeGen.Phase.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.driver(CodeGen.Phase.FinalizeIR); fail(); } + catch( Exception e ) { assertEquals("'Test._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=2; val y=4; }; +_Point p = new _Point; +p.x++; +return p; +"""); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Cannot modify final field '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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Test._Point;", code.print()); + assertEquals("Test._Point{x=4,y=4}", Eval2.eval(code, 0)); + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter17bTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter17bTest.java new file mode 100644 index 00000000..62dbbde4 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter17bTest.java @@ -0,0 +1,621 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Chapter17bTest { + + @Test public void testSinkLoopStores() { + var code = new CodeGen(""" + struct S { int x; }; + !S !a = new S; + a.x = 10; + int i = 0; + while (i < arg) { a.x = i + 1; i++; } + return a; + """).driver(CodeGen.Phase.LocalSched); + int[] stores = {0}; + code._stop.walk(n -> { + // Constructor zero Stores remain in separate private memory. + if( n instanceof com.seaofnodes.simple.node.StoreNode st && st._name.equals("x") && + st.val() instanceof com.seaofnodes.simple.node.PhiNode ) { + assertEquals(0,st.cfg0().loopDepth()); // One final Store outside the loop. + stores[0]++; + } + return null; + }); + assertEquals(1,stores[0]); + for( int arg : new int[]{-1,0,1,4} ) { + long expected = arg<=0 ? 10 : arg; + assertEquals("Test.S{x="+expected+"}",Eval2.eval(code,arg)); + } + } + + + @Ignore + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 1; +"""); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + + // --------------------------------------------------------------- + @Test + public void testInc0() { + CodeGen code = new CodeGen(""" +return arg++; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return arg;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + + @Test + public void testInc1() { + CodeGen code = new CodeGen(""" +return arg+++arg++; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 65535;", code.print()); + assertEquals("65535", Eval2.eval(code, 2)); + } + + @Test + public void testInc6() { + CodeGen code = new CodeGen("return --arg;"); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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;"); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("2",Eval2.eval(code,0)); + } + + + // --------------------------------------------------------------- + @Test public void testVar0() { + CodeGen code = new CodeGen("var d; return d;"); + try { code.driver(CodeGen.Phase.FinalizeIR); fail(); } + catch( Exception e ) { assertEquals("Syntax error, expected `=expression` but found `;`",e.getMessage()); } + } + + @Test public void testVar1() { + CodeGen code = new CodeGen("val d; return d;"); + try { code.driver(CodeGen.Phase.FinalizeIR); fail(); } + catch( Exception e ) { assertEquals("Syntax error, expected `=expression` but found `;`",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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test public void testVar5() { + CodeGen code = new CodeGen("struct _S{int x; new _S={ int xx -> x=xx; };}; !_S? !s; s=new _S(3); s.x++; return s.x; // Ok, no initializer so x is mutable "); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.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; new S={ int xx -> x=xx; };}; ~S s = new S(3); s.x++; return s.x; // Error initializer so x is immutable "); + try { code.driver(CodeGen.Phase.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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + @Test public void testVar10() { + CodeGen code = new CodeGen("struct S{int x;}; ~S s=new S; s.x++; return s.x; // Error, has val so x is immutable "); + try { code.driver(CodeGen.Phase.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; new Foo = { !Bar? b -> bar = b; }; }; +!Foo !foo = new Foo(bar); +foo.bar = bar; // Ok foo is mutable +foo.bar.x++; // Ok foo and foo.bar and foo.bar.x are all mutable + +~Foo xfoo = foo; // Throw away mutability +xfoo.bar.x++; // Error, cannot mutate through xfoo +return xfoo.bar.x; + """); + try { code.driver(CodeGen.Phase.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 + +~_Foo xfoo = foo; // Throw away mutability +int x4 = xfoo.bar.x; // Ok to read through xfoo, gets 4 +foo.bar.x++; // Bumps to 5 +int x5 = xfoo.bar.x; // Ok to read through xfoo, gets 5 +return x4*10+x5; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 45;", code.print()); + assertEquals("45", Eval2.eval(code, 0)); + } + + @Test + public void testVar13() { + CodeGen code = new CodeGen(""" +int i,i++; +"""); + try { code.driver(CodeGen.Phase.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; new A = { -> + return b; // read before init + b = new B; +}; }; +A x = new A; +"""); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Field 'b' might not be initialized",e.getMessage()); } + } + + @Test + public void testVar15() { + for( String fields : new String[] { + "!B !b; new A = { -> if (arg) b = new B; };", + "!B !b; new A = { -> if (arg) return 0; b = new B; };", + "!B !b; new A = { !B !b -> b = new B; };", // Mutable argument shadows field + "new A = { -> }; !B !b;", // Field declared later + } ) { + CodeGen code = new CodeGen("struct B {}; struct A { "+fields+" }; return 0;"); + try { code.parse(); fail(fields); } + catch( Parser.ParseException e ) { assertEquals("'Test.A' is not fully initialized, field 'b' is only partially set in the constructor",e.getMessage()); } + } + } + + @Test + public void testDefaultConstructorFields() { + for( String field : new String[] { "!B !b;", "!B !b;" } ) { + CodeGen code = new CodeGen("struct B {}; struct A { "+field+" }; return new A;"); + try { code.parse(); fail(field); } + catch( Parser.ParseException e ) { assertEquals("'Test.A' is not fully initialized, field 'b' needs to be set in a constructor",e.getMessage()); } + } + for( String fields : new String[] { + "B b = new B;", + "!B !b; b = new B;", + "new A = { -> }; B b = new B;", + "!B !b; new A = { -> if (arg) { b = new B; return 0; } b = new B; };", + } ) { + CodeGen code = new CodeGen("struct B {}; struct A { "+fields+" }; return new A.b != null;"); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + assertEquals("1",Eval2.eval(code,1)); + } + // No returning path means no object can escape uninitialized. + new CodeGen("struct B {}; struct A { !B !b; new A = { -> while (true) {} }; }; return 0;").parse(); + } + + @Test + public void testVar16() { + CodeGen code = new CodeGen( +""" +struct S{ new S = { -> x = 2; }; }; +val x = 1; +~S s = new S; +return x; +""" +); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Undefined name '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(); fail(); } + catch( Exception e ) { assertEquals("Syntax error, expected `=expression` but found `;`",e.getMessage()); } + } + + // --------------------------------------------------------------- + @Test + public void testTrinary0() { + CodeGen code = new CodeGen(""" +return arg ? 1 : 2; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + + @Test + public void testShortCircuitGuard() { + CodeGen code = new CodeGen(""" +struct S { int !x; }; +val get = { !S s -> s.x; }; +!S? !s = arg ? new S; +if( !s || !arg ) return 0; +return get(s); +"""); + code.driver(CodeGen.Phase.TypeCheck); + } + + @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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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; new _Bar = { !_Bar? !n, int xx -> next=n; x=xx; }; }; +var b = arg ? new _Bar((arg==2) ? new _Bar(null,2), 1); +return b ? b.next ? b.next.x : b.x; // parses "b ? (b.next ? b.next.x : b.x) : 0" +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Region,.x,0,.x);", 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Cannot return generic scalar",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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR); fail(); } + catch( Exception e ) { assertEquals("Undefined name 'i'",e.getMessage()); } + } + @Test + public void testFor5() { + CodeGen code = new CodeGen(""" +for(;;arg++;) {} +"""); + try { code.driver(CodeGen.Phase.FinalizeIR); fail(); } + catch( Exception e ) { assertEquals("Syntax error, expected `Unexpected code after expression` but found `;`",e.getMessage()); } + } + + + // --------------------------------------------------------------- + @Test + public void testForward0() { + CodeGen code = new CodeGen(""" +struct _A { + !B? !f1; + !B? !f2; +}; +return new _A; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Test._A;", code.print()); + assertEquals("Test._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?[] !array_of_b; + new _A = { !B?[] bs -> array_of_b = bs; }; +}; +return new _A(new !B?[0]).array_of_b; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return []*B?;", code.print()); + assertEquals("*B {}?[]", Eval2.eval(code,0)); + } + + // --------------------------------------------------------------- + @Test + public void testLinkedList2() { + CodeGen code = new CodeGen(""" +struct _LLI { !_LLI? !next; int i; new _LLI = { !_LLI? !n, int ii -> next=n; i=ii; }; }; +!_LLI? !head = null; +while( arg-- ) + head = new _LLI(head,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.driver(CodeGen.Phase.FinalizeIR); + 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.driver(CodeGen.Phase.FinalizeIR).driver(CodeGen.Phase.Schedule); + assertEquals("return []i64;", code.print()); + assertEquals("i64[ 2,3,5,7,11,13,17,19]",Eval2.eval(code, 20)); + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter18Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter18Test.java new file mode 100644 index 00000000..ae1dca5f --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter18Test.java @@ -0,0 +1,522 @@ +package com.seaofnodes.simple; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.node.*; + + +import com.seaofnodes.simple.codegen.CodeGen.Phase; +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.junit.Assert.fail; + +public class Chapter18Test { + @Test public void testNeverExitLoopDepth() { + String[] sources = { + "if(arg) while(1) {} return 7;", + "if(arg==1) while(1) {} if(arg==2) while(1) {} return 7;", + "while(1) {}", + "int x=0; while(arg) { if(arg==2) while(1) { x++; } x++; arg--; } return x;" + }; + for( String src : sources ) { + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.LoopTree); + int[] nevers = {0}; + code._stop.walk(n -> { + if( n instanceof FunNode fun ) + assertEquals("Function entry is outside its loops",0,fun.loopDepth()); + if( n instanceof ReturnNode ret ) { + assertEquals(ret.fun().loopDepth(),ret.loopDepth()); + assertEquals(ret.fun().loopDepth(),ret.cfg0().loopDepth()); + } + if( n instanceof LoopNode loop && !(loop instanceof StartNode) && + loop.back() instanceof CProjNode back && back.in(0) instanceof NeverNode never ) { + nevers[0]++; + assertSame(loop,never.loop()); + assertSame(loop,back.loop()); + CProjNode exit = never.cproj(1-back._idx); + CFGNode fun = loop; + while( !(fun instanceof FunNode) ) fun=fun.idom(); + assertSame(fun.loop(),exit.loop()); + assertEquals(fun.loopDepth(),exit.loopDepth()); + assertTrue(loop.loopDepth()>fun.loopDepth()); + } + return null; + }); + assertTrue("Exercise a synthetic exit",nevers[0]>0); + } + } + + @Test public void testFunctionLocalConstantChains() { + String src = "val f = { int x -> x ? f(x-1)*305420988+x/305420988 : 1; }; "+ + "val g = { int x -> x ? g(x-1)/305420988+x*305420988 : 1; }; "+ + "return f(arg)+g(arg);"; + CodeGen code = new CodeGen(src); + code.driver(CodeGen.Phase.Schedule,"riscv","SystemV"); + var owners = new java.util.IdentityHashMap<CFGNode,Boolean>(); + code._stop.walk(n -> { + if( !(n instanceof com.seaofnodes.simple.node.cpus.riscv.AddIRISC add) || + !(add.in(1) instanceof com.seaofnodes.simple.node.cpus.riscv.LUI upper) || + ((TypeInteger)upper._con).value()+((add._imm12<<20)>>20)!=305420988 ) + return null; + CFGNode fun = n.cfg0(); + while( fun!=null && !(fun instanceof FunNode) ) fun = fun.idom(); + assertTrue("Constant must belong to a function",fun instanceof FunNode); + assertNull("Share one constant chain within each function",owners.put(fun,true)); + // Follow all parts of a machine constant, including shared inputs. + var todo = new java.util.ArrayList<Node>(); + var seen = new java.util.IdentityHashMap<Node,Boolean>(); + todo.add(n); + for( int j=0; j<todo.size(); j++ ) { + Node part = todo.get(j); + if( seen.put(part,true)!=null ) continue; + CFGNode owner = part.cfg0(); + while( owner!=null && !(owner instanceof FunNode) ) owner = owner.idom(); + assertSame("Every constant-building operation is function-local",fun,owner); + for( int i=1; i<part.nIns(); i++ ) { + Node def = part.in(i); + if( def==null || def instanceof CFGNode ) continue; + assertTrue("Copies must register their data edges",def._outputs.find(part)!=-1); + if( def._type.isConstant() ) todo.add(def); + } + } + return null; + }); + assertTrue("Exercise constants shared across functions",owners.size()>=2); + } + + @Test public void testReachableMixedReturns() { + for( String[] test : new String[][] { + {"if(arg) return 7; else return 2.5;", "No common type amongst int and f64"}, + {"struct S { int x; }; if(arg) return new S; else return 0;", + "No common type amongst int and reference"} + } ) { + try { + new CodeGen(test[0]).driver(CodeGen.Phase.TypeCheck); + fail("Expected incompatible return types: " + test[0]); + } catch( Parser.ParseException e ) { + assertEquals("Cannot return generic scalar",e.getMessage()); + } + } + } + + + @Test public void testPrintingForwardReferenceScope() throws Exception { + new CodeGen("return 0;").parse(); + var declared = TypeMemPtr.make(TypeStruct.make("PrintForward",true,true)); + var scope = new ScopeNode(); + scope.define("ptr",declared,false,new ConstantNode(TypeInteger.ZERO),null); + Parser.TYPES.put("PrintForward",TypeMemPtr.make(TypeStruct.make("PrintForward",false,false))); + var field = Var.class.getDeclaredField("_type"); + field.setAccessible(true); + var v = scope.var(0); + org.junit.Assert.assertSame(declared,field.get(v)); + scope.toString(); + org.junit.Assert.assertSame(declared,field.get(v)); + } + + + @Ignore + @Test + public void testJig() { + CodeGen code = new CodeGen( +""" +return 0; +"""); + code.driver(Phase.LocalSched); + assertEquals("return 0;", code.print()); + 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.driver(CodeGen.Phase.TypeCheck); + assertEquals("return Phi(Loop,1,Phi(Loop,2,Phi_a));", code.print()); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return null;", code.print()); + assertEquals("null", Eval2.eval(code, 0 ) ); + } + + @Test + public void testFcn0() { + CodeGen code = new CodeGen( +""" +{int -> int}? sq = { int x -> + x*x; +}; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("Stop[ return { sq}; return (Parm_x(sq,i64)*x); ]", code.print()); + assertEquals("{ ptr i64 -> i64 #[ 3]}", Eval2.eval(code, 3)); + } + + @Test + public void testFcn1() { + CodeGen code = new CodeGen( +""" +var _sq = { int x -> + x*x; +}; +return _sq(arg)+_sq(3); +"""); + code.driver(Phase.LocalSched); + assertEquals("return ((arg*arg)+9);", code.print()); + assertEquals("13", Eval2.eval(code, 2)); + } + + // Function scope test + @Test + public void testFcn2A() { + CodeGen code = new CodeGen( +""" +int cnt=1; // Global scope; exactly one of these +return { -> cnt++; }; // Escape global-scope variable is OK +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("Stop[ return { ptr -> i64 #[ 3]}; return .cnt; ]", code.print()); + assertEquals("{ ptr -> i64 #[ 3]}", Eval2.eval(code, 0)); + } + + // Function scope test + @Test + public void testFcn2B() { + CodeGen code = new CodeGen( +""" +val ctr = { -> + int cnt; // Function-local var + return { -> cnt=cnt+1; cnt; }; // Escape cnt out of scope +}; +val A = ctr(); // Make a counter +val B = ctr(); // Make a counter +A(); // Increment counter A +return A()*10 + B(); // Incr A and B; result: 2*10+1 == 21 +"""); + try { code.driver(CodeGen.Phase.FinalizeIR); 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return 2;", code.print()); + 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.driver(CodeGen.Phase.FinalizeIR); + assertEquals("Stop[ return #2; return (Parm_x($fun3,i64,3)*x); return (Parm_x($fun4,i64,3)<<1); ]", code.print()); + 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.driver(CodeGen.Phase.TypeCheck); + String ir = code.print(); + assertTrue(ir.equals("Stop[ return #2; return Phi(Region,1,(Parm_x(_fact,arg,(x-1))*#2)); ]") || + ir.equals("Stop[ return Phi(Region,1,(arg*#2)); return Phi(Region,1,(Parm_x(_fact,(arg-1),(x-1))*#2)); ]") || + ir.equals("Stop[ return Phi(Region,1,(arg*Phi(Region,1,((arg-1)*#2)))); return Phi(Region,1,(Parm_x(_fact,(Sub-1),(x-1))*#2)); ]")); + 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)); + } + + // The multiply initially sees one integer input and one unresolved + // The multiply initially sees one integer input and one unresolvedfcn8 + // recursive result, but the floating base case requires a floating mode. + @Test + public void testFcn5Float() { + CodeGen code = new CodeGen("val _fact = { int x -> x <= 1 ? 1.0 : x*_fact(x-1); }; return _fact(arg);"); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("1.0", Eval2.eval(code, 0)); + assertEquals("1.0", Eval2.eval(code, 1)); + assertEquals("2.0", Eval2.eval(code, 2)); + assertEquals("6.0", Eval2.eval(code, 3)); + assertEquals("24.0", Eval2.eval(code, 4)); + } + + @Test + public void testFcn5Minus() { + CodeGen code = new CodeGen("val _neg = { int x -> x ? -_neg(x-1) : 1; }; return _neg(arg);"); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals( "1", Eval2.eval(code, 0)); + assertEquals("-1", Eval2.eval(code, 1)); + assertEquals( "1", Eval2.eval(code, 2)); + assertEquals("-1", Eval2.eval(code, 3)); + } + + @Test + public void testFcn6() { + CodeGen code = new CodeGen( +""" +struct _S { int i; new _S = { int x -> i=x; }; }; +val _newS = { int x -> return new _S(x); }; +return _newS(1).i; +"""); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + } + + // Only forward refs to direct function calls allowed + @Test + public void testFcn7() { + CodeGen code = new CodeGen( +""" +val _f = {->1;}; +val _g = {->2;}; +if( arg ? _f : _g ) return 1; +return 2; +"""); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("return 1;", code.print()); + 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.driver(Phase.LocalSched); + assertEquals("return Phi(Loop,arg,3);", code.print()); + 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.driver(Phase.LocalSched); + assertEquals("Stop[ return Top; return Parm_i(x,i64); ]", code.print()); + assertNull( Eval2.eval( code, 0 ) ); // Infinite loop, <clinit> never exits + } + + + @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; +return _fcn(ps,1); +"""); + code.driver(Phase.LocalSched); + assertEquals("return .age;", code.print()); + 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.driver(CodeGen.Phase.FinalizeIR); 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; +} +return 0; +"""); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Might be null calling { ptr i64 -> i64 #[ 3]}?",e.getMessage()); } + } + + + @Test + public void testErr3() { + CodeGen code = new CodeGen( +""" +val f = { int i, int j -> return i+j; }; +return f(); +"""); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Expecting 3 arguments, but found 1",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 = new S; +return s.x; +"""); + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Accessing unknown field 'x' from '*Test.S'",e.getMessage()); } + } + + @Test @Ignore + public void testErr5() { + CodeGen code = new CodeGen( +""" +val g = { -> + g(); +}; +return g(); +"""); + try { code.driver(CodeGen.Phase.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.driver(CodeGen.Phase.TypeCheck); + assertEquals("Stop[ return #2; return Phi(Region,0,1,#2); ]", code.print()); + 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.driver(CodeGen.Phase.FinalizeIR).driver(CodeGen.Phase.Schedule); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); + } + + // Inline hidden called more than once + @Test + public void testInline() { + CodeGen code = new CodeGen( +""" +{int->int}?! _i2i_noInline = {int i->return i;}; +{{int->int}->{int->int}}! _f2f = {{int->int} f->return f;}; +val _o = _i2i_noInline; +if (arg) _i2i_noInline = null; +if (_i2i_noInline) return _i2i_noInline(arg); +return _f2f(_o)(1); +"""); + code.driver(Phase.LocalSched); + assertEquals("Stop[ return Phi(Region,#2,#2); return Parm_i(_i2i_noInline,i64); ]", code.print()); + 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.driver(Phase.LocalSched); + assertEquals("return 1;", code.print()); + assertEquals("1", Eval2.eval(code, 2)); + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter19Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter19Test.java new file mode 100644 index 00000000..6f290279 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter19Test.java @@ -0,0 +1,459 @@ +package com.seaofnodes.simple; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + + +import com.seaofnodes.simple.codegen.CodeGen.Phase; +import com.seaofnodes.simple.codegen.CodeGen; +import java.io.IOException; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Chapter19Test { + @Test public void testPrintingFunctionLookup() { + var code = new CodeGen("return 0;").parse(); + var tfp = TypeFunPtr.make1((byte)2,false,new Type[]{TypeInteger.constant(987654)},TypeInteger.constant(123456),30); + var con = ConstantNode.raw(tfp); + var call = new CallNode(null,code._start,con,con); + // Diagnostic lookup may construct/intern the return-erased linker key. + con.toString(); + org.junit.Assert.assertNull(call.name()); + + var fun = new FunNode(null,tfp,"printerTarget",null); + fun._name = "printerTarget"; + fun.addDef(code._start); + code.link(fun); + var otherReturn = tfp.makeFrom(TypeInteger.constant(654321)); + org.junit.Assert.assertSame(fun,code._link(otherReturn)); + org.junit.Assert.assertTrue(con.toString().contains("printerTarget")); + assertEquals("printerTarget",call.name()); + } + + + @Ignore + @Test + public void testJig() { + CodeGen code = new CodeGen(""" +return 0; +"""); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 2)); + } + + + @Test + public void testString() { + String src = +""" +struct String { + u8[~] !cs; + int _hashCode; + new String = { u8[~] s -> cs=s; }; +}; + +// Compare two Strings +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 _hashCodeString = { !String self -> + int hash=0; + for( int i=0; i< self.cs#; i++ ) + hash = hash*31 + self.cs[i]; + if( !hash ) hash = 123456789; + return hash; +}; + +// Return the String hashCode (cached, and never 0) +val hashCode = { !String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + +hashCode(new String("Hello, World!")); +"""; + CodeGen code = new CodeGen(src).driver(Phase.LocalSched); + assertEquals("Stop[ return Phi(Region,123456789,(!=0)Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash)))); return MEM[ 2:___ 3:___ 4:___ 5:.cs=Parm_s(Test.String.String,*[]u8[final]); 6:._hashCode=0;]; return MEM[ 2:___ 3:___ 4:___ 5:___ 6:._hashCode=0;]; return Test.String; return Phi(Region,0,1,0,1); return Phi(Region,(!=0)._hashCode,Phi(Region,123456789,(!=0)Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash))))); ]", code.print()); + assertEquals("4029215624828139541", Eval2.eval(code, 2)); + } + + @Test + public void testTernaryStoreCall() { + String src = +""" +struct String { + u8[~] !cs; + int _hashCode; + new String = { u8[~] s -> cs=s; }; +}; + +val f = { !String self -> 7; }; + +val hashCode = { !String self -> + self._hashCode + ? 3 + : (self._hashCode = f(self)); +}; + +hashCode(new String("Hello")); +"""; + CodeGen code = new CodeGen(src).driver(Phase.LocalSched); + assertFalse(code.print().contains("return Top")); + assertEquals("7", Eval2.eval(code, 2)); + } + + @Test + public void testBasic0() { + CodeGen code = new CodeGen("return 0;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return 0;", code.print()); + } + + @Test + public void testBasic1() { + CodeGen code = new CodeGen("return arg+1;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (inc,arg);", code.print()); + } + + @Test + public void testBasic2() { + CodeGen code = new CodeGen("return -17;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return -17;", code.print()); + } + + + @Test + public void testBasic3() { + CodeGen code = new CodeGen("return arg==1;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (set==,(cmp,arg));", code.print()); + } + + @Test + public void testBasic4() { + CodeGen code = new CodeGen("return arg<<1;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (shli,arg);", code.print()); + } + + @Test + public void testBasic5() { + CodeGen code = new CodeGen("return arg >> 1;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (sari,arg);", code.print()); + } + + @Test + public void testBasic6() { + CodeGen code = new CodeGen("return arg >>> 1;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (shri,arg);", code.print()); + } + + @Test + public void testBasic7() { + CodeGen code = new CodeGen("return arg / 2;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (div,arg,2);", code.print()); + } + + @Test + public void testBasic8() { + CodeGen code = new CodeGen("return arg * 6;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (muli,arg);", code.print()); + } + + @Test + public void testBasic9() { + CodeGen code = new CodeGen("return arg & 2;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (andi,arg);", code.print()); + } + + @Test + public void testBasic10() { + CodeGen code = new CodeGen("return arg | 2;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (ori,arg);", code.print()); + } + + @Test + public void testBasic11() { + CodeGen code = new CodeGen("return arg ^ 2;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (xori,arg);", code.print()); + } + + @Test + public void testBasic12() { + CodeGen code = new CodeGen("return arg + 2.0;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (addf,(cvtf,arg),2.0f);", code.print()); + } + + @Test + public void testBasic13() { + CodeGen code = new CodeGen("return arg - 2.0;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (subf,(cvtf,arg),2.0f);", code.print()); + } + + @Test + public void testBasic14() { + CodeGen code = new CodeGen("return arg * 2.0;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (mulf,(cvtf,arg),2.0f);", code.print()); + } + + @Test + public void testBasic15() { + CodeGen code = new CodeGen("return arg / 2.0;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (mulf,(cvtf,arg),0.5f);", code.print()); + } + + @Test + public void testBasic16() { + CodeGen code = new CodeGen( +""" +int arg1 = arg + 1; +return arg1 / arg;"""); + code.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (div,(inc,arg),arg);", code.print()); + } + + @Test + public void testBasic17() { + CodeGen code = new CodeGen( +""" +int arg1 = arg + 1; +return arg1 * arg; +"""); + code.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (mul,(inc,arg),arg);", code.print()); + } + + @Test + public void testToFloat() { + CodeGen code = new CodeGen(""" +int a = arg; +return a + 2.0; +""" + ).driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (addf,(cvtf,arg),2.0f);", code.print()); + } + + @Test + public void testIfStmt() { + CodeGen code = new CodeGen( +""" +int a = 1; +if (arg == 1) + a = arg+2; +else { + a = arg-3; +} +return a;"""); + code.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + 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.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + 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.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + 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.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return Test._S;", code.print()); + } + + @Test + public void testLea1() { + CodeGen code = new CodeGen("int x = arg/3; return arg+x+7;"); + code.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + 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.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + 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.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + 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.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + 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.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + 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.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + 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]; +return 0; +"""); + code.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + 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.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return .[];", code.print()); + } + + @Test + public void testNewton() throws IOException { + String src = +""" +val _test_sqrt = { flt x -> + flt epsilon = 1e-15; + flt guess = x; + while( 1 ) { + flt next = (x/guess + guess)/2; + if( guess-epsilon <= next & next <= guess+epsilon ) return guess; + //if( guess==next ) return guess; + guess = next; + } +}; +flt farg = arg; +return _test_sqrt(farg); +"""; + CodeGen code = new CodeGen(src).driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return Phi(Loop,(cvtf,arg),(mulf,(addf,Phi_guess,(divf,cvtf,Phi_guess)),0.5f));", code.print()); + }; + + + @Test + public void sieveOfEratosthenes() { + String src = +""" +val sieve = { int N -> + // The main Sieve array + bool[] !ary = new bool[N]; + // The primes less than N + u32[] !primes = new u32[N>>1]; + // Number of primes so far, searching at index p + int nprimes = 0, p=2; + // Find primes while p^2 < N + while( p*p < N ) { + // 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; + p++; + } + + // Now just collect the remaining primes, no more marking + for( ; p < N; p++ ) + if( !ary[p] ) + primes[nprimes++] = p; + + // Copy/shrink the result array + u32[] !rez = new u32[nprimes]; + for( int j=0; j < nprimes; j++ ) + rez[j] = primes[j]; + return rez; +}; +"""; + CodeGen code = new CodeGen(src).driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("Stop[ return { sieve}; return []u32; ]", code.print()); + //assertEquals("{ ptr i64 -> *[]u32 #[ 3]}",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.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("Stop[ return (add,#2,(muli,#2)); return (mul,Parm_x($fun3,[2-3]),x); return (shli,Parm_x($fun4,[2-3])); ]", code.print()); + } + + @Test + public void testFcn2() { + CodeGen code = new CodeGen( +""" +val _sq = { int x -> x*x; }; +return _sq(arg) + _sq(3); +"""); + code.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); + assertEquals("return (addi,(mul,arg,arg));", code.print()); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter20Test.java new file mode 100644 index 00000000..cdfd16a7 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter20Test.java @@ -0,0 +1,306 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; + + +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Chapter20Test { + @Test public void testAllocatorTopPhi() { com.seaofnodes.simple.codegen.RegAllocTestSupport.topPhi(); } + + @Test public void testNeverReturnAllocation() { + String[] sources = { + // A real integer return merged with a synthetic exit. + "if(arg) while(1) {} return arg+1;", + // TOP must not widen a non-null pointer or constrain a float register. + "struct S { int x; }; !S !s=new S; if(arg) while(1) {} return s;", + "if(arg) while(1) {} return 1.5;", + // Several synthetic exits into the same return. + "if(arg==1) while(1) {} if(arg==2) while(1) {} return arg+3;", + // No real return, including a merge of only synthetic exits. + "while(1) { arg+=1; }", + "if(arg) while(1) {} while(1) {}", + // Loop-carried memory and scalar self-conflicts exercise spilling. + "struct S { int x; int y; }; !S !a=new S; if(arg) while(1) { a.x+=arg; a.y+=a.x; } return a.x+a.y;" + }; + for( String target : new String[]{"x86_64_v2","riscv","arm"} ) + for( String src : sources ) { + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.RegAlloc,target,"SystemV"); + com.seaofnodes.simple.codegen.RegAllocTestSupport.checkRegisters(code); + assertNotNull(code._stop.walk(n -> { + if( n._type!=com.seaofnodes.simple.type.Type.TOP ) return null; + assertEquals(-1,code._regAlloc.regnum(n)); + for( var use : n.outs() ) + assertFalse(use instanceof com.seaofnodes.simple.node.SplitNode); + return n; + })); + code.encode(); + } + } + + @Test public void testAllocatorFixedNeighbor() throws Exception { com.seaofnodes.simple.codegen.RegAllocTestSupport.uncoloredFixedNeighbor(); } + + @Test public void testNarrowStoreMasks() { + for( String type : new String[]{"i8","u8","i16","u16"} ) { + CodeGen code = new CodeGen("struct S { "+type+" x; }; !S !s=new S; s.x=arg; return 0;") + .driver(CodeGen.Phase.RegAlloc,"x86_64_v2","SystemV"); + int stores=0; + for( var bb : code._cfg ) + for( var node : bb.outs() ) + if( node instanceof com.seaofnodes.simple.node.cpus.x86_64_v2.StoreX86 st && st._name.equals("x") ) { + stores++; + assertEquals(-1,st.regmap(4).nextReg((short)15)); + } + assertTrue(stores>0); + } + } + + @Test public void testInlinedReturnValue() { + String src = """ +struct S { int x; }; +val f = { !S s -> s.x = g(); }; +val g = { -> 123; }; +!S !s = new S; +return f(s); +"""; + // The entry of g can disappear before its Return is folded into f. + // Exercise both worklist orders; deleting the entry does not kill 123. + for( int seed=0; seed<64; seed++ ) { + CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck); + assertEquals("seed "+seed, "123", Eval2.eval(code,0)); + } + } + + + @Test public void testAllocatorMasks() { com.seaofnodes.simple.codegen.RegAllocTestSupport.masks(); } + @Test public void testAllocatorUnion() { com.seaofnodes.simple.codegen.RegAllocTestSupport.union(); } + @Test public void testAllocatorCopyClobber() throws Exception { com.seaofnodes.simple.codegen.RegAllocTestSupport.copyClobber(); } + @Test public void testAllocatorCommutativePhi() { com.seaofnodes.simple.codegen.RegAllocTestSupport.commutativePhi(); } + @Test public void testAllocatorKills() { com.seaofnodes.simple.codegen.RegAllocTestSupport.killWithoutResult(); } + @Test public void testAllocatorDependencies() { com.seaofnodes.simple.codegen.RegAllocTestSupport.nullUseMask(); } + @Test public void testAllocatorCloneClass() { com.seaofnodes.simple.codegen.RegAllocTestSupport.cloneRegisterClass(); } + + @Test public void testPrintingRegisters() throws Exception { + com.seaofnodes.simple.codegen.PrintRegTestSupport.check(); + } + + + @Ignore + @Test + public void testJig() { + CodeGen code = new CodeGen("return 0;"); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 2)); + } + + static void testCPU( String src, String cpu, String os, int spills, String stop ) { + CodeGen code = new CheckedCodeGen(src); + code.driver(CodeGen.Phase.RegAlloc,cpu,os); + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); + if( stop != null ) + assertEquals(stop, code.print()); + } + + private static void testAllCPUs( String src, int spills, String stop ) { + testCPU(src,"x86_64_v2", "SystemV",spills,stop); + testCPU(src,"riscv" , "SystemV",spills,stop); + testCPU(src,"arm" , "SystemV",spills,stop); + } + + @Test public void testAlloc0() { + testCPU("return new u8[arg];","x86_64_v2", "SystemV",3,"return []u8;"); + testCPU("return new u8[arg];","riscv" , "SystemV",5,"return []u8;"); + testCPU("return new u8[arg];","arm" , "SystemV",5,"return []u8;"); + } + + @Test public void testBasic1() { + String src = "return arg | 2;"; + testCPU(src,"x86_64_v2", "SystemV",1,"return mov((ori,arg));"); + testCPU(src,"riscv" , "SystemV",0,"return ( arg | #2 );"); + testCPU(src,"arm" , "SystemV",0,"return (ori,arg);"); + } + + @Test + public void testNewtonInteger() { + String src = +""" +// Newtons approximation to the square root +val _sqrt = { int x -> + int guess = x; + while( 1 ) { + int next = (x/guess + guess)>>1; + if( next == guess ) return guess; + guess = next; + } +}; +return _sqrt(arg) + _sqrt(arg+2); +"""; + testCPU(src,"x86_64_v2", "win64" ,38,null); + testCPU(src,"riscv" , "SystemV",19,null); + testCPU(src,"arm" , "SystemV",19,null); + } + + @Test + public void testNewtonFloat() { + String src = +""" +val _test_sqrt_noInline = { flt x -> + flt epsilon = 1e-15; + flt guess = x; + while( 1 ) { + flt next = (x/guess + guess)/2; + if( guess-epsilon <= next && next <= guess+epsilon ) + return guess; + guess = next; + } +}; +flt farg = arg; return _test_sqrt_noInline(farg) + _test_sqrt_noInline(farg+2.0); +"""; + testCPU(src,"x86_64_v2", "SystemV",40,null); + testCPU(src,"riscv" , "SystemV",17,null); + testCPU(src,"arm" , "SystemV",18,null); + } + + @Test + public void testIterPrettyPrintIsLocalAndReadOnly() { + CodeGen code = new CodeGen(""" + val f_noInline = { flt x -> while(1) { if(x) return x; } }; + flt y=arg; + return f_noInline(y)+f_noInline(y+2.0); + """).driver(CodeGen.Phase.Iter); + assertNull(code._start._ltree); + int pre = code._start._pre; + + String p0 = code.toString(); + String p1 = code.toString(); + assertEquals(p0,p1); + assertNull(code._start._ltree); + assertEquals(pre,code._start._pre); + + int main0 = p0.indexOf("--- class:Test.<clinit>"); + int main1 = p0.indexOf("--- class:Test.<clinit> ----------------------",main0); + int fun0 = p0.indexOf("--- f_noInline ",main1); + int fun1 = p0.indexOf("--- f_noInline ----------------------",fun0); + assertTrue(main0 < main1 && main1 < fun0 && fun0 < fun1); + assertTrue(p0.indexOf("ToFloat",main0) < p0.indexOf("Call",main0)); + assertTrue(p0.indexOf("CallEnd",fun0) == -1 || p0.indexOf("CallEnd",fun0) > fun1); + } + + @Test + public void testAlloc2() { + String src = "int[] !xs = new int[3]; xs[arg]=1; return xs[arg&1];"; + testCPU(src,"x86_64_v2", "SystemV",-1,"return .[];"); + testCPU(src,"riscv" , "SystemV", 6,"return .[];"); + testCPU(src,"arm" , "SystemV", 6,"return .[];"); + } + + + @Test + public void testArray1() { + String src = +""" +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 +"""; + testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);"); + testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));"); + testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));"); + } + + + @Test + public void testString() { + String src = +""" +struct String { + u8[] !cs; + int _hashCode; + new String = { u8[] data -> cs=data; }; +}; + +// Compare two Strings +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; +}; + +// Return the String hashCode (cached, and never 0) +val hashCode = { !String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + +val _hashCodeString = { !String self -> + int hash=0; + for( int i=0; i< self.cs#; i++ ) + hash = hash*31 + self.cs[i]; + if( !hash ) hash = 123456789; + return hash; +}; +"""; + testCPU(src,"x86_64_v2", "SystemV",12,null); + testCPU(src,"riscv" , "SystemV",6,null); + testCPU(src,"arm" , "SystemV",6,null); + } + + @Test + public void testCast() { + String src = "struct Bar { int x; }; var b = arg ? new Bar; return b ? b.x++ + b.x++ : -1;"; + testCPU(src,"x86_64_v2", "SystemV",3,null); + testCPU(src,"riscv" , "SystemV",6,null); + testCPU(src,"arm" , "SystemV",6,null); + } + + @Test + public void testFltArg() { + String src = "return {int i, flt f, int j->return i+f+j;};"; + testAllCPUs(src,0,null); + } + + @Test + public void testFlags1() { + String src = """ +bool _b1 = arg == 1; +bool b2 = arg == 2; +if (b2) if (_b1) return 1; +if (_b1) return 2; +return 0; +"""; + testCPU(src,"x86_64_v2", "SystemV",0,"return Phi(Region,2,0,1);"); + testCPU(src,"riscv" , "SystemV",0,"return Phi(Region,2,0,1);"); + testCPU(src,"arm" , "SystemV",0,"return Phi(Region,2,0,1);"); + } + + @Test + public void testFlags2() { + String src = """ +bool b1 = arg == 1; +while (arg > 0) { + arg--; + if (b1) arg--; +} +return arg; +"""; + testCPU(src,"x86_64_v2", "SystemV",3,null); + testCPU(src,"riscv" , "SystemV",2,null); + testCPU(src,"arm" , "SystemV",2,null); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter21Test.java new file mode 100644 index 00000000..2fcf65e9 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -0,0 +1,715 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.isa.eval.EvalRisc5; + +import com.seaofnodes.isa.eval.EvalArm64; + +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.cpus.arm.arm; +import com.seaofnodes.simple.node.cpus.riscv.riscv; +import com.seaofnodes.simple.util.SB; +import java.io.IOException; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Chapter21Test { + + @Test public void testFloatToInteger() throws IOException { + String src = "val float_to_int={ flt x -> int i=x; return i; };"+ + "val float_to_u8={ flt x -> u8 i=x; return i; };"; + TestC.runC(src,"floatToInteger","",-1); + for( double x : new double[]{0.0,-0.0,6.92,-6.92,255.75,-257.75, + 0x1p63,Math.nextDown(0x1p63),-0x1p63,Math.nextDown(-0x1p63), + Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY,Double.NaN} ) { + for( String name : new String[]{"float_to_int","float_to_u8"} ) { + long expected = name.equals("float_to_u8") ? ((long)x)&255 : (long)x; + EvalRisc5 r = TestRisc5.build(src,name,0,-1,false); + r.fregs[riscv.FA0-riscv.F_OFFSET]=x; + assertEquals(0,r.step(1000)); + assertEquals(name+"("+x+")",expected,r.regs[riscv.A0]); + EvalArm64 a = TestArm64.build(name,src,0,-1,false); + a.fregs[arm.D0-arm.D_OFFSET]=x; + assertEquals(0,a.step(1000)); + assertEquals(name+"("+x+")",expected,a.regs[arm.X0]); + } + } + } + + @Test public void testReadBeforeConditionalStore() { + var code = new CodeGen(""" + struct S { int x; }; !S !a=new S; !S !b=new S; + a.x=10; b.x=20; !S !p=a; if(arg&1) p=b; + int sum=0; + for(int i=0; i<6; i++) { + sum+=a.x; + if(i==2) p.x=30; + else if(i==4) b.x=40; + } + return sum; + """).driver(CodeGen.Phase.FinalizeIR); + // The i==2 read still sees 10: three reads of 10, then three of 30. + assertEquals("120",Eval2.eval(code,0)); + assertEquals("60",Eval2.eval(code,1)); + } + + + @Test public void testArmBitfieldExtracts() { + // Independent SXT/UXT instruction words, plus ASR, LSR and LSL aliases. + int[] ops = {0x93401c20,0xd3401c20,0x93403c20,0xd3403c20, + 0x93407c20,0xd3407c20,0x9344fc20,0xd344fc20,0xd37cec20}; + long value = 0xa5a5a5a580008080L; + long[] expected = {(byte)value,value&255,(short)value,value&65535, + (int)value,value&0xffffffffL,value>>4,value>>>4,value<<4}; + for( int i=0; i<ops.length; i++ ) { + EvalArm64 cpu = new EvalArm64(new byte[1<<16],1<<15); + cpu.st4(0,ops[i]); cpu.regs[1]=value; + assertEquals(0,cpu.step(1)); + assertEquals(expected[i],cpu.regs[0]); + } + } + + + @Test public void testArmFloatMemory() throws IOException { + for( String type : new String[]{"f32","f64"} ) { + String src = type+"[] !a=new "+type+"[3]; a[arg]=1.25; return a[1];"; + EvalArm64 cpu = TestArm64.build("probe","val probe={ int arg -> "+src+" };",1,-1,false); + cpu.regs[arm.X0]=1; + assertEquals(0,cpu.step(1000)); + assertEquals(1.25,cpu.fregs[0],0); + } + // Independently exercise both SIMD widths and addressing modes. Keep + // the GPR value different so accidentally reading that bank is visible. + for( boolean wide : new boolean[]{false,true} ) + for( boolean indexed : new boolean[]{false,true} ) { + EvalArm64 cpu = new EvalArm64(new byte[512],256); + cpu.regs[1]=128; cpu.regs[2]=16; cpu.regs[3]=99; + cpu.fregs[3]=-13.25; + int store = (wide ? 0xFD000000 : 0xBD000000) | ((16/(wide?8:4))<<10) | (1<<5) | 3; + if( indexed ) store=(wide ? 0xFC226823 : 0xBC226823); // STR D3/S3,[X1,X2] + cpu.st4(0,store); + cpu.st4(4,store | (1<<22)); // LDR to the same SIMD register. + assertEquals(0,cpu.step(1)); + assertEquals(-13.25,wide ? cpu.ld8f(144) : cpu.ld4f(144),0); + cpu.fregs[3]=0; + assertEquals(0,cpu.step(1)); + assertEquals(-13.25,cpu.fregs[3],0); + } + } + + + @Test public void testArmCallInstructions() { + EvalArm64 cpu = new EvalArm64(new byte[512],256); + for( int delta : new int[]{4,32,-4} ) { + cpu._pc = 128; + for( int i=0; i<32; i++ ) cpu.regs[i]=1000+i; + cpu.st4(128,0x94000000 | ((delta>>2)&0x03FFFFFF)); // BL + assertEquals(0,cpu.step(1)); + assertEquals(128+delta,cpu._pc); + for( int i=0; i<32; i++ ) assertEquals(i==30 ? 132 : 1000+i,cpu.regs[i]); + } + for( int rn : new int[]{9,30} ) + for( boolean call : new boolean[]{false,true} ) { + cpu._pc=4; + cpu.regs[rn]=64; + cpu.st4(4,(call ? 0xD63F0000 : 0xD65F0000) | (rn<<5)); // BLR / RET + assertEquals(0,cpu.step(1)); + assertEquals(64,cpu._pc); + if( call ) assertEquals(8,cpu.regs[30]); + } + } + + @Test public void testArmImmediateArithmetic() { + EvalArm64 cpu = new EvalArm64(new byte[16],16); + // Unsigned imm12, including bit 11 and the optional 12-bit shift. + for( int[] test : new int[][]{{0x91200020,2148},{0xD1200020,-1948}, + {0x91400420,4196},{0xD1400420,-3996}} ) { + cpu._pc=0; cpu.regs[1]=100; + cpu.N=true; cpu.Z=false; cpu.C=true; cpu.V=true; + cpu.st4(0,test[0]); + assertEquals(0,cpu.step(1)); + assertEquals(test[1],cpu.regs[0]); + assertTrue(cpu.N); assertFalse(cpu.Z); assertTrue(cpu.C); assertTrue(cpu.V); + } + } + + @Test public void testArmCallsAndFrames() throws IOException { + String src = """ + val sum = { int n -> + if( n<=0 ) return 1; + return sum(n-1)+n; + }; + val run = { int n -> return sum(n)+sum(n+1); }; + """; + assertTrue("Ordinary calls must survive optimization",checkArmCalls(src,"run",4,27)>0); + } + + @Test public void testArmVectorGrowth() throws IOException { + String src = """ + struct _Vec { + u32 !len; + int[] !buf; + new _Vec = { int[] initial -> buf=initial; }; + val grow = { int sz -> + var buf2 = new int[sz]; + for( int i=0; i<len; i++ ) buf2[i]=buf[i]; + buf=buf2; + }; + val add = { int e -> + if( len>=buf# ) grow(buf#*2); + buf[len++]=e; + return self; + }; + }; + val run = { int arg -> + val v = new _Vec(new int[4]).add(2).add(3).add(5).add(7).add(11); + return v.len*100+v.buf[arg]; + }; + """; + checkArmCalls(src,"run",4,511); + } + + private static int checkArmCalls(String src, String entryName, int arg, int result) throws IOException { + CodeGen code = new CodeGen(src).driver("arm","SystemV",true,false); + byte[] image = new byte[1<<20]; + System.arraycopy(code._encoding.bits(),0,image,0,code._encoding.bits().length); + EvalArm64 cpu = new EvalArm64(image,1<<16); + boolean entry=false; + int frames=0, calls=0; + for( var bb : code._cfg ) { + if( bb instanceof com.seaofnodes.simple.node.FunNode fun ) { + int off = code._encoding.opStart(fun); + if( entryName.equals(fun._name) ) { cpu._pc=off; entry=true; } + int frame=fun._frameAdjust; + assertEquals(0,frame&15); + if( frame>0 ) { + frames++; + assertEquals(0xD10003FF | (frame<<10),cpu.ld4s(off)); // SUB SP,SP,#bytes + } + } + if( bb instanceof com.seaofnodes.simple.node.ReturnNode ret ) { + int off=code._encoding.opStart(ret); + int frame=ret.fun()._frameAdjust; + if( frame>0 ) { + assertEquals(0x910003FF | (frame<<10),cpu.ld4s(off)); // ADD SP,SP,#bytes + off+=4; + } + assertEquals(0xD65F03C0,cpu.ld4s(off)); // RET X30 + } + if( bb instanceof com.seaofnodes.simple.node.cpus.arm.CallARM ) calls++; + } + assertTrue("Must execute the program entry",entry); + assertTrue("Exercise stack frames",frames>0); + for( int i=19; i<=29; i++ ) cpu.regs[i]=1000+i; + cpu.regs[0]=arg; + assertEquals(0,cpu.step(10000)); + assertEquals("Must return to the harness",0,cpu._pc); + assertEquals(result,cpu.regs[0]); + assertEquals(1<<16,cpu.regs[31]); + for( int i=19; i<=29; i++ ) assertEquals(1000+i,cpu.regs[i]); + return calls; + } + + + @Test public void testCoalescing() { com.seaofnodes.simple.codegen.RegAllocTestSupport.coalescing(); } + @Test public void testCopyForward() throws Exception { com.seaofnodes.simple.codegen.RegAllocTestSupport.copyForward(); } + + @Test public void testArmSubtractRegisters() { + EvalArm64 cpu = new EvalArm64(new byte[16],16); + cpu.st4(0,0xCB020020); // SUB X0,X1,X2, no shift or flag update. + for( long[] pair : new long[][]{{7,3},{3,7},{Long.MIN_VALUE,1},{1L<<40,3}} ) { + cpu._pc = 0; + cpu.regs[1] = pair[0]; cpu.regs[2] = pair[1]; + cpu.N=true; cpu.Z=false; cpu.C=true; cpu.V=true; + assertEquals(0,cpu.step(1)); + assertEquals(pair[0]-pair[1],cpu.regs[0]); + assertTrue(cpu.N); assertFalse(cpu.Z); assertTrue(cpu.C); assertTrue(cpu.V); + } + } + + + @Test public void testRisc64BitStore() { + byte[] mem = new byte[24]; + EvalRisc5 cpu = new EvalRisc5(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + @Test public void testArm64BitStore() { + byte[] mem = new byte[24]; + EvalArm64 cpu = new EvalArm64(mem,mem.length); + check64BitStore(mem,cpu::st8,cpu::ld8); + } + + // Check bytes independently of ld8, including overwrite and adjacent memory. + private static void check64BitStore(byte[] mem, + java.util.function.BiConsumer<Integer,Long> store, + java.util.function.IntToLongFunction load) { + java.util.Arrays.fill(mem,(byte)0xA5); + for( long value : new long[]{0x0123456789ABCDEFL,0xFEDCBA9876543210L, + -1L,0L,Long.MIN_VALUE,Long.MAX_VALUE} ) { + store.accept(8,value); + for( int i=0; i<mem.length; i++ ) { + int expected = i>=8 && i<16 ? (int)(value >>> (8*(i-8)))&0xFF : 0xA5; + assertEquals("byte "+i,expected,mem[i]&0xFF); + } + assertEquals(value,load.applyAsLong(8)); + } + } + + + @Ignore + @Test + public void testJig() throws IOException { + String src = +""" +struct _s0 { + bool v1; + i16 v2; + int v3; + i8 v4; + byte v5; +}; +while(new _s0.v3) + while(new _s0.v5<<new _s0.v4) {} +if(0) { + if(0) { + flt !P5ZUD4=new _s0.v2; + } + while(0) {} +} +return new _s0.v1; +"""; + testCPU(src,"x86_64_v2", "win64" ,-1,null); + testCPU(src,"riscv" , "SystemV",-1,null); + testCPU(src,"arm" , "SystemV",-1,null); + } + + static void testCPU( String src, String cpu, String os, int spills, String stop ) { + CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); + if( stop != null ) + assertEquals(stop, code.print()); + } + + + @Test public void testBasic1() { + String src = "return arg | 2;"; + testCPU(src,"x86_64_v2", "SystemV",1,"return mov((ori,arg));"); + testCPU(src,"riscv" , "SystemV",0,"return ( arg | #2 );"); + testCPU(src,"arm" , "SystemV",0,"return (ori,arg);"); + } + + @Test public void testInfinite() { + String src = "struct _S { int i; }; !_S !s = new _S; while(1) s.i++; return s.i;"; + testCPU(src,"x86_64_v2", "SystemV",0,"return Top;"); + testCPU(src,"riscv" , "SystemV",0,"return Top;"); + testCPU(src,"arm" , "SystemV",0,"return Top;"); + } + + @Test + public void testArray1() { + String src = +""" +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 +"""; + testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);"); + testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));"); + testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));"); + } + + @Test + public void testAntiDeps1() { + String src = +""" +struct _S { int f; }; +var v0 = new _S; +!_S? !v1; +if (arg) v1 = new _S; +if (v1) { + v0.f = v1.f; +} else { + v0.f = 2; +} +return v0; +"""; + testCPU(src,"x86_64_v2", "SystemV", 9,"return mov(mov(Test._S));"); + testCPU(src,"riscv" , "SystemV",10,"return mov(mov(Test._S));"); + testCPU(src,"arm" , "SystemV",11,"return mov(mov(Test._S));"); + } + + @Test + public void testString() { + String src = +""" +struct String { + u8[] !cs; + int _hashCode; + new String = { u8[] data -> cs=data; }; +}; + +// Compare two Strings +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; +}; + +// Return the String hashCode (cached, and never 0) +val hashCode = { !String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + +val _hashCodeString = { !String self -> + int hash=0; + for( int i=0; i< self.cs#; i++ ) + hash = hash*31 + self.cs[i]; + if( !hash ) hash = 123456789; + return hash; +}; +"""; + testCPU(src,"x86_64_v2", "SystemV",12,null); + testCPU(src,"riscv" , "SystemV",6,null); + testCPU(src,"arm" , "SystemV",6,null); + } + + @Test public void testStringExport() throws IOException { + String src = +""" +struct String { + u8[] !cs; + int _hashCode; + new String = { u8[] data -> cs=data; }; +}; + +// Compare two Strings +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; +}; + +// Return the String hashCode (cached, and never 0) +val hashCode = { !String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + +val _hashCodeString = { !String self -> + int hash=0; + for( int i=0; i< self.cs#; i++ ) + hash = hash*31 + self.cs[i]; + if( !hash ) hash = 123456789; + return hash; +}; +"""; + TestC.runC(src, "stringHash", "",12); + } + + @Test public void testLoop2() { + String src = +""" +int i = 0; +while(true) { + i += 1; + if( i==0 ) continue; + if( i==1 ) continue; + if( i==2 ) break; + continue; +} +return i; +"""; + testCPU(src,"x86_64_v2", "win64" ,0,"return (!=0)(inc,Phi(Loop,0,Phi(Region,(!=0),(!=0),0)));"); + testCPU(src,"riscv" , "SystemV",0,"return (!=0)( Phi(Loop,0,Phi(Region,(!=0),(!=0),0)) + #1 );"); + testCPU(src,"arm" , "SystemV",0,"return (!=0)(inc,Phi(Loop,0,Phi(Region,(!=0),(!=0),0)));"); + } + + @Test public void testNewtonExport() throws IOException { + String result = """ +0 0.000000 (0) +1 1.000000 (0) +2 1.414214 (2.22045e-16) +3 1.732051 (0) +4 2.000000 (0) +5 2.236068 (0) +6 2.449490 (0) +7 2.645751 (0) +8 2.828427 (4.44089e-16) +9 3.000000 (0) +"""; + String src = +""" +val test_sqrt = { flt x -> + flt epsilon = 1e-15; + flt guess = x; + while( 1 ) { + flt next = (x/guess + guess)/2; + if( guess-epsilon <= next & next <= guess+epsilon ) return guess; + //if( guess==next ) return guess; + guess = next; + } +}; +"""; + TestC.runC(src, "newtonFloat", result, TestC.CALL_CONVENTION.equals("win64") ? 42 : 34); + + EvalRisc5 R5 = TestRisc5.build( src, "test_sqrt", 0, 10, false); + R5.fregs[riscv.FA0 - riscv.F_OFFSET] = 3.0; + int trap_r5 = R5.step(1000); + assertEquals(0,trap_r5); + // Return register A0 holds fib(8)==55 + assertEquals(1.732051,R5.fregs[riscv.FA0 - riscv.F_OFFSET], 0.00001); + + // arm + EvalArm64 A5 = TestArm64.build("test_sqrt", src,0, 10, false); + A5.fregs[arm.D0 - arm.D_OFFSET] = 3.0; + int trap_arm = A5.step(1000); + assertEquals(0,trap_arm); + assertEquals(1.732051, A5.fregs[arm.D0 - arm.D_OFFSET], 0.00001); + } + + + @Test public void testSieve() throws IOException { + String src = +""" +val sieve = { int N -> + // The main Sieve array + bool[] !ary = new bool[N]; + // The primes less than N + u32[] !primes = new u32[N>>1]; + // Number of primes so far, searching at index p + int nprimes = 0, p=2; + // Find primes while p^2 < N + while( p*p < N ) { + // 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; + p++; + } + + // Now just collect the remaining primes, no more marking + for( ; p < N; p++ ) + if( !ary[p] ) + primes[nprimes++] = p; + + // Copy/shrink the result array + u32[] !rez = new u32[nprimes]; + for( int j=0; j < nprimes; j++ ) + rez[j] = primes[j]; + return rez; +}; +"""; + // The primes + int[] primes = new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, }; + SB sb = new SB().p(primes.length).p("["); + for( int prime : primes ) + sb.p(prime).p(", "); + String sprimes = sb.p("]").toString(); + + // Compile, link against native C; expect the above string of primes to be printed out by C + TestC.runC(src, "sieve", sprimes, 186); + + // Evaluate on RISC5 emulator; expect return of an array of primes in + // the simulated heap. + EvalRisc5 R5 = TestRisc5.build( src, "sieve", 100, 90, false); + int trap = R5.step(10000); + assertEquals(0,trap); + // Return register A0 holds sieve(100) + int ary = (int)R5.regs[riscv.A0]; + // Memory layout starting at ary(length,pad, prime1, primt2, prime3, prime4) + assertEquals(primes.length, R5.ld4s(ary)); + for( int i=0; i<primes.length; i++ ) + assertEquals(primes[i], R5.ld4s(ary + 4 + i*4)); + + // Evaluate on ARM5 emulator; expect return of an array of primes in + // the simulated heap. + EvalArm64 A5 = TestArm64.build("sieve", src, 100, 88, false); + int trap_arm = A5.step(10000); + assertEquals(0, trap_arm); + int ary_arm = (int)A5.regs[arm.X0]; + // Memory layout starting at ary(length,pad, prime1, primt2, prime3, prime4) + assertEquals(primes.length, A5.ld4s(ary_arm)); + for( int i = 0; i<primes.length; i++ ) + assertEquals(primes[i], A5.ld4s(ary_arm + 4 + i * 4)); + } + + @Test public void testFibExport() throws IOException { + String src = +""" +val fib = { int n -> + int f1=1; + int f2=1; + while( n-- > 1 ){ + int temp = f1+f2; + f1=f2; + f2=temp; + } + return f2; +}; +"""; + String fib = "[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]"; + TestC.runC(src, "fib", fib, 24); + + EvalRisc5 R5 = TestRisc5.build( src, "fib", 9, 17, false); + int trap = R5.step(100); + assertEquals(0,trap); + // Return register A0 holds fib(8)==55 + assertEquals(55,R5.regs[riscv.A0]); + + // arm + EvalArm64 A5 = TestArm64.build("fib", src, 9, 17, false); + int trap_arm = A5.step(100); + assertEquals(0,trap_arm); + // Return register X0 holds fib(8)==55 + assertEquals(55, A5.regs[arm.X0]); + } + + @Test public void testPerson() throws IOException { + String src = + """ + struct Person { + i32 age; + }; + + val fcn = { !Person?[] !ps, int x -> + if( ps[x] ) + ps[x].age++; + }; + """; + String person = "6\n"; + TestC.run(src, "person1", null, TestC.CALL_CONVENTION, "", TestC.C_DRIVERS_DIR+"person.c", person, 2); + + // Memory layout starting at PS: + int ps = 1<<16; // Person array pointer starts at heap start + // Person[3] = { len,pad,P0,P1,P2 }; // sizeof = 4*8 + // P0 = { age } // sizeof=8 + int p0 = ps+4*8+0*8; + // P1 = { age } // sizeof=8 + int p1 = ps+4*8+1*8; + // P2 = { age } // sizeof=8 + int p2 = ps+4*8+2*8; + EvalRisc5 R5 = TestRisc5.build( src, "fcn", ps, 2, false); + R5.regs[riscv.A1] = 1; // Index 1 + R5.st8(ps,3); // Length + R5.st8(ps+1*8,p0); + R5.st8(ps+2*8,p1); + R5.st8(ps+3*8,p2); + R5.st8(p0, 5); // age= 5 + R5.st8(p1,17); // age=17 + R5.st8(p2,60); // age=60 + + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals( 5+0,R5.ld8(p0)); + assertEquals(17+1,R5.ld8(p1)); + assertEquals(60+0,R5.ld8(p2)); + + EvalArm64 A5 = TestArm64.build("fcn", src, ps, 2, false); + A5.regs[arm.X1] = 1; // Index 1 + A5.st8(ps, 3); + A5.st8(ps+1*8,p0); + A5.st8(ps+2*8,p1); + A5.st8(ps+3*8,p2); + A5.st8(p0, 5); // age= 5 + A5.st8(p1,17); // age=17 + A5.st8(p2,60); // age=60 + + int trap_arm = A5.step(100); + assertEquals(0,trap_arm); + assertEquals( 5+0, A5.ld8(p0)); + assertEquals(17+1, A5.ld8(p1)); + assertEquals(60+0, A5.ld8(p2)); + } + + @Test public void testArgCount() throws IOException { + // Test passes more args than registers in Sys5, which is far, far more + // than what win64 allows - so win64 gets a lot more spills here. + String src = +""" +val addAll = { int i0, flt f1, int i2, flt f3, int i4, flt f5, int i6, flt f7, int x8, flt f9, int i10, flt f11, int i12, flt f13, int i14, flt f15, int x16, flt f17 int x18, flt f19 -> + return + i0 + f1+ i2+ f3+ i4+ f5+ i6+ f7+ x8 +f9 + + i10+f11+i12+f13+i14+f15+x16+f17+x18+f19 ; +}; +"""; + String arg_count = "191.000000\n"; + + TestC.runC(src, "arg_count", arg_count, TestC.CALL_CONVENTION.equals("win64") ? 35 : 9); + + EvalRisc5 R5 = TestRisc5.build( src, "addAll", 0, 4, false); + + // Todo: handle stack(imaginary stack in emulator) + // pass in float arguments + R5.fregs[riscv.FA0 - riscv.F_OFFSET] = 1.1; + R5.fregs[riscv.FA1 - riscv.F_OFFSET] = 1.1; + R5.fregs[riscv.FA2 - riscv.F_OFFSET] = 1.1; + R5.fregs[riscv.FA3 - riscv.F_OFFSET] = 1.1; + R5.fregs[riscv.FA4 - riscv.F_OFFSET] = 1.1; + R5.fregs[riscv.FA5 - riscv.F_OFFSET] = 1.1; + R5.fregs[riscv.FA6 - riscv.F_OFFSET] = 1.1; + R5.fregs[riscv.FA7 - riscv.F_OFFSET] = 1.1; + + R5.regs[riscv.A0] = 2; + R5.regs[riscv.A1] = 2; + R5.regs[riscv.A2] = 2; + R5.regs[riscv.A3] = 2; + R5.regs[riscv.A4] = 2; + R5.regs[riscv.A5] = 2; + R5.regs[riscv.A6] = 2; + + int trap = R5.step(100); + assertEquals(0,trap); + + double result = R5.fregs[riscv.FA0 - riscv.F_OFFSET]; + + assertEquals(22.8, result, 0.00001); + + // arm + EvalArm64 A5 = TestArm64.build("addAll", src, 0, 4, false); + + A5.fregs[arm.D0 - arm.D_OFFSET] = 1.1; + A5.fregs[arm.D1 - arm.D_OFFSET] = 1.1; + A5.fregs[arm.D2 - arm.D_OFFSET] = 1.1; + A5.fregs[arm.D3 - arm.D_OFFSET] = 1.1; + A5.fregs[arm.D4 - arm.D_OFFSET] = 1.1; + A5.fregs[arm.D5 - arm.D_OFFSET] = 1.1; + A5.fregs[arm.D6 - arm.D_OFFSET] = 1.1; + A5.fregs[arm.D7 - arm.D_OFFSET] = 1.1; + + A5.regs[arm.X1] = 2; + A5.regs[arm.X2] = 2; + A5.regs[arm.X3] = 2; + A5.regs[arm.X4] = 2; + A5.regs[arm.X5] = 2; + A5.regs[arm.X6] = 2; + A5.regs[arm.X7] = 2; + + int trap_arm = A5.step(100); + assertEquals(0,trap_arm); + + double result1 = A5.fregs[arm.D0 - arm.D_OFFSET]; + assertEquals(22.8, result1, 0.00001); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter22Test.java new file mode 100644 index 00000000..00d77f65 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter22Test.java @@ -0,0 +1,374 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.isa.eval.EvalRisc5; + +import com.seaofnodes.isa.eval.EvalArm64; + +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.cpus.arm.arm; +import com.seaofnodes.simple.node.cpus.riscv.riscv; +import java.io.IOException; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Chapter22Test { + + @Test public void testNarrowCReturnsEmulated() { + String[] types = {"i8","u8","i16","u16","i32","u32","int"}; + long[] values = {0xa5a5a5a500000000L,0xa5a5a5a50000007fL,0xa5a5a5a500000080L, + 0xa5a5a5a500007fffL,0xa5a5a5a500008000L,0xa5a5a5a57fffffffL, + 0xa5a5a5a580000000L,0xa5a5a5a5ffffffffL}; + for( String target : new String[]{"arm","riscv"} ) + for( int i=0; i<types.length; i++ ) { + String src = "{"+types[i]+"} c_narrow=\"C\"; return c_narrow();"; + CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,target,"SystemV"); + var enc = code._encoding; + // Link the external call to a tiny independent native stub. + var call = code._stop.walk(n -> n instanceof com.seaofnodes.simple.node.CallNode ? n : null); + int start = enc.opStart(call); + ((com.seaofnodes.simple.codegen.RIPRelSize)call).patch(enc,start,enc.opLen(call),0x1000-start); + for( long value : values ) { + long expected = switch(i) { + case 0 -> (byte)value; case 1 -> value&255; + case 2 -> (short)value; case 3 -> value&65535; + case 4 -> (int)value; case 5 -> value&0xffffffffL; + default -> value; + }; + byte[] image = new byte[1<<16]; + byte[] bits = enc.bits(); + System.arraycopy(bits,0,image,0,bits.length); + if( target.equals("arm") ) { + EvalArm64 cpu = new EvalArm64(image,1<<15); + // MOVZ X0,0x1100; LDR X0,[X0]; RET X30. + cpu.st4(0x1000,0xd2822000); cpu.st4(0x1004,0xf9400000); + cpu.st4(0x1008,0xd65f03c0); + cpu.st8(0x1100,value); + assertEquals(0,cpu.step(1000)); + assertEquals(0,cpu._pc); + assertEquals(types[i],expected,cpu.regs[0]); + } else { + EvalRisc5 cpu = new EvalRisc5(image,1<<15); + // AUIPC a0,0; LD a0,12(a0); RET. + cpu.st4(0x1000,0x00000517); cpu.st4(0x1004,0x00c53503); + cpu.st4(0x1008,0x00008067); + // RV64 widens even u32 returns by sign-extending bit 31. + cpu.st8(0x100c,i==6 ? value : (int)expected); + assertEquals(0,cpu.step(1000)); + assertEquals(0,cpu._pc); + assertEquals(types[i],expected,cpu.regs[riscv.A0]); + } + } + } + } + + @Test public void testNarrowCReturns() throws IOException { + String src = """ + {i8} c_i8="C"; {u8} c_u8="C"; + {i16} c_i16="C"; {u16} c_u16="C"; + {i32} c_i32="C"; {u32} c_u32="C"; + {int} c_i64="C"; + val test_i8={->return c_i8();}; + val test_u8={->return c_u8();}; + val test_i16={->return c_i16();}; + val test_u16={->return c_u16();}; + val test_i32={->return c_i32();}; + val test_u32={->return c_u32();}; + val test_i64={->return c_i64();}; + val test_cmp={->return c_i32()==-1;}; + """; + for( String conv : new String[]{"SystemV","win64"} ) + TestC.run(src,"c_returns"+conv,null,conv, + conv.equals("SystemV") ? "sysv_abi" : "ms_abi", + TestC.C_DRIVERS_DIR+"c_returns.c","",-1); + } + + + @Test public void testSubZeroTypeError() { + try { + new CodeGen("return null-0;").driver(CodeGen.Phase.TypeCheck); + fail("Subtraction must reject null even when the other operand is zero"); + } catch( Parser.ParseException e ) { + assertEquals("Cannot null - 0",e.getMessage()); + } + } + + + @Ignore + @Test + public void testJig() throws IOException { + String src = """ +struct s0 { + bool v1; + i16 v2; + int v3; + i8 v4; + byte v5; +}; +while(new s0.v3) + while(new s0.v5<<new s0.v4) {} +if(0) { + if(0) { + flt !P5ZUD4=new s0.v2; + } + while(0) {} +} +return new s0.v1; +"""; + testCPU(src,"x86_64_v2", "win64" ,-1,null); + testCPU(src,"riscv" , "SystemV",-1,null); + testCPU(src,"arm" , "SystemV",-1,null); + } + + static CodeGen testCPU( String src, String cpu, String os, int spills, String stop ) { + CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); + if( stop != null ) + assertEquals(stop, code.print()); + return code; + } + + + static int testCPUSize( String src, String cpu, String os, int spills ) { + // Includes the file initializer's hidden $init=true store. + return testCPU(src,cpu,os,spills, "return 0;" )._encoding._bits.size(); + } + + // Should not fold away + @Test public void testSextFail() throws IOException { + String src = """ +struct _Person { i32 age;}; +!_Person !p = new _Person; +p.age = (arg<<17)>>17; +return 0; +"""; + assertEquals(76, testCPUSize(src, "x86_64_v2","win64", 3 )); + assertEquals(92, testCPUSize(src, "riscv", "SystemV", 4 )); + assertEquals(92, testCPUSize(src, "arm" , "SystemV", 4 )); + + // do assertEquals here + EvalRisc5 R5 = TestRisc5.build( src, "sext_str_not_fold_away", 0, 4, false); + int trap = R5.step(100); + assertEquals(0,trap); + + EvalArm64 A5 = TestArm64.build("sext_str_not_fold_away", src, 0, 4, false); + trap = A5.step(100); + assertEquals(0,trap); + + } + + // Should not fold away + @Test public void testSextFail2() throws IOException { + String src = """ + struct _Person { i32 age;}; + !_Person !p = new _Person; + p.age = (arg<<48)>>48; + return 0; + """; + + EvalRisc5 R5 = TestRisc5.build( src, "sext_str_not_fold_away_2", 0, 4, false); + int trap = R5.step(100); + assertEquals(0,trap); + + EvalArm64 A5 = TestArm64.build("sext_str_not_fold_away_2", src, 0, 3, false); + int trap_arm = A5.step(100); + assertEquals(0,trap_arm); + + assertEquals(76, testCPUSize(src, "x86_64_v2","win64",3 )); + assertEquals(92, testCPUSize(src, "riscv", "SystemV",4 )); + assertEquals(92, testCPUSize(src, "arm", "SystemV",4 )); + + } + + // Should fold away + @Test public void testSextSuccess() throws IOException { + String src = """ +// Should fold away sign extend +struct _Person { i8 age;}; +!_Person !p = new _Person; +p.age = (arg<<48)>>48; +return 0; +"""; + + EvalRisc5 R5 = TestRisc5.build( src, "sext_str_fold_away", 0, 5, false); + int trap = R5.step(100); + assertEquals(0,trap); + + EvalArm64 A5 = TestArm64.build("sext_str_fold_away", src, 0, 5, false); + int trap_arm = A5.step(100); + assertEquals(0,trap_arm); + + assertEquals(66, testCPUSize(src, "x86_64_v2","win64",3 )); + assertEquals(84, testCPUSize(src, "riscv", "SystemV",5 )); + assertEquals(84, testCPUSize(src, "arm", "SystemV",5)); + // do assertEquals here + } + + + // Int now is changed to 4 bytes. + @Test public void testPerson() throws IOException { + String src = +""" +struct Person { // Exports a field typed singleton+final *class:Person{age:i32} + i32 age; +}; + +val fcn = { !Person?[] !ps, int x -> // exports a field with a constant fcn ptr; exports the fcn also + if( ps[x] ) + ps[x].age++; +}; +"""; + String person = "6\n"; + TestC.run(src, "person2", null, TestC.CALL_CONVENTION, "", TestC.C_DRIVERS_DIR+"person.c", person, 2); + + // Memory layout starting at PS: + int ps = 1<<16; // Person array pointer starts at heap start + // Person[3] = { len,pad,P0,P1,P2 }; // sizeof = 4*8 + // P0 = { age } // sizeof=8 + int p0 = ps+4*8+0*8; + // P1 = { age } // sizeof=8 + int p1 = ps+4*8+1*8; + // P2 = { age } // sizeof=8 + int p2 = ps+4*8+2*8; + EvalRisc5 R5 = TestRisc5.build( src, "fcn", ps, 2, false); + R5.regs[riscv.A1] = 1; // Index 1 + R5.st8(ps,3); // Length + R5.st8(ps+1*8,p0); + R5.st8(ps+2*8,p1); + R5.st8(ps+3*8,p2); + R5.st8(p0, 5); // age= 5 + R5.st8(p1,17); // age=17 + R5.st8(p2,60); // age=60 + + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals( 5+0,R5.ld8(p0)); + assertEquals(17+1,R5.ld8(p1)); + assertEquals(60+0,R5.ld8(p2)); + + EvalArm64 A5 = TestArm64.build("fcn", src, ps, 2, false); + A5.regs[arm.X1] = 1; // Index 1 + A5.st8(ps, 3); + A5.st8(ps+1*8,p0); + A5.st8(ps+2*8,p1); + A5.st8(ps+3*8,p2); + A5.st8(p0, 5); // age= 5 + A5.st8(p1,17); // age=17 + A5.st8(p2,60); // age=60 + + int trap_arm = A5.step(100); + assertEquals(0,trap_arm); + assertEquals( 5+0, A5.ld8(p0)); + assertEquals(17+1, A5.ld8(p1)); + assertEquals(60+0, A5.ld8(p2)); + } + + @Test + public void testCoRecur() { + String src = """ +val x = 5; // aa.az; // Error to self-define forward ref +struct _A { !Test._B? !b; !Test._C? !c; i64 ax; val az = x*2; new _A = { i64 x -> ax=x; }; }; +struct _B { !Test._A? !a; !Test._C? !c; f32 bx; val bz = x*3; new _B = { f32 x, !Test._A? aa -> bx=x; a=aa; }; }; +struct _C { !Test._A? !a; !Test._B? !b; f64 cx; val cz = x*x; new _C = { f64 x, !Test._A? aa, !Test._B? bb -> cx=x; a=aa; b=bb; }; }; +!_A !aa = new _A(17); +!_B !bb = new _B(3.14f, aa); +!_C !cc = new _C(2.73, aa, bb); +aa.b = bb; +aa.c = cc; +bb.c = cc; +return cc.cz; +"""; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("return 25;", code.print()); + assertEquals("25", Eval2.eval(code, 0)); + } + + @Test + public void testHelloWorld() throws IOException { + String src = """ +// fd buf len -> len +{ i32 i64 u32 -> u32 } write = "C"; +// Write byte array to stdout +val p = { u8[~] str -> + i64 ptr = str; // cast array base to i64 + return write(1,ptr,str#); +}; +p("Hello, World!"); +return 0; +"""; + TestC.runSF(src,"helloWorld", "Hello, World!",2); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src, "helloWorld", 0, 5, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(0,R5.regs[riscv.A0]); + assertEquals("Hello, World!",R5._stdout.toString()); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("helloWorld", src,0, 5, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(0,arm.regs[0]); + assertEquals("Hello, World!",arm._stdout.toString()); + } + + + @Test + public void testStrLoad() { + CodeGen code = new CodeGen("return \"abc\"[1];"); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return .[];", code.print()); + } + + + @Test + public void testFinalArray() { + String src = """ +int N=4; +i32[] !is = new i32[N]; +for( int i=0; i<N; i++ ) + is[i] = i*i; +val _sum = { i32[~] is -> // final array + int sum=0; + for( int i=0; i<is#; i++ ) + sum += is[i]; + return sum; +}; +return _sum(is); +"""; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("return Phi(Loop,0,(Phi_sum+.[]));", code.print()); + assertEquals("14", Eval2.eval(code, 0)); + } + + + @Test @Ignore + public void testEcho() throws IOException { + String src = +""" +// Echo stdin to stdout. +return sys.io.p( sys.io.stdin() ); +"""; + TestC.runSF(src,"echo", "",0); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src, "echo", 0, 2, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(0,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("echo", src, 0, 2, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(0,arm.regs[0]); + } + + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter23Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter23Test.java new file mode 100644 index 00000000..df02d3b4 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter23Test.java @@ -0,0 +1,341 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.isa.eval.EvalRisc5; + +import com.seaofnodes.isa.eval.EvalArm64; + +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.cpus.riscv.riscv; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Chapter23Test { + @Test public void testPopularUses() { com.seaofnodes.simple.codegen.RegAllocTestSupport.popularUses(); } + + @Test public void testDiagnosticTypeAccessors() throws Exception { + new CodeGen("return 0;").parse(); + var tfp = TypeFunPtr.TEST; + var con = ConstantNode.raw(tfp); + var call = new CallNode(null,con,con,con); + var field = Type.class.getDeclaredField("VISIT"); + field.setAccessible(true); + var visit = (java.util.Map<Object,Type>)field.get(null); + var marker = new Object(); + visit.put(marker,Type.BOTTOM); + try { + assertEquals(0,TypeInteger.ZERO.alignment()); + assertEquals(0,TypeInteger.ZERO.value()); + org.junit.Assert.assertTrue(tfp._isConstant()); + con.toString(); + call.name(); + con.p(1); + org.junit.Assert.assertSame(Type.BOTTOM,visit.get(marker)); + assertEquals(1,visit.size()); + } finally { visit.clear(); } + } + + + @Test @Ignore + public void testJig() throws IOException { + //String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/jig.smp")); + String src = Files.readString(Path.of("docs/examples/BubbleSort.smp")); + testCPU(src,"x86_64_v2", "win64" ,-1,null); + testCPU(src,"riscv" , "SystemV",-1,null); + testCPU(src,"arm" , "SystemV",-1,null); + } + + + static CodeGen testCPU( String src, String cpu, String os, int spills, String stop ) { + CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); + if( stop != null ) + assertEquals(stop, code.print()); + return code; + } + + @Test + public void testOr() throws IOException { + String src = """ +int a = 1; +int b = 0; +int rez = 2; + +if( a++ || b++ ) { + if( b == 0 && a == 2 ) + rez = 0; // Expected answer +} else { + rez = 1; +} +return rez; +"""; + + TestC.runSF( src, "or1", "", 0); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src, "or1", 0, 0, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(0,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("or1", src,0, 0, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(0,arm.regs[0]); + } + + @Test + public void testAnd() throws IOException { + String src = """ +int a = 1; +int b = 1; +return a && b ? 0 : 1; // Expected answer 0 +"""; + TestC.runSF( src, "and1", "", 0); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src, "and1", 0, 0, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(0,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("and1", src, 0, 0, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(0,arm.regs[0]); + } + + @Test + public void testLogicalValueSemantics() { + assertEquals("42", Eval2.eval(new CodeGen("return 7 && 42;").driver(CodeGen.Phase.FinalizeIR), 0)); + assertEquals("0", Eval2.eval(new CodeGen("return 0 && 42;").driver(CodeGen.Phase.FinalizeIR), 0)); + assertEquals("7", Eval2.eval(new CodeGen("return 7 || 42;").driver(CodeGen.Phase.FinalizeIR), 0)); + assertEquals("42", Eval2.eval(new CodeGen("return 0 || 42;").driver(CodeGen.Phase.FinalizeIR), 0)); + } + + @Test + public void testMixedScalarNotPhiPredicate() { + CodeGen code = new CodeGen(""" +struct S {}; +S? s = arg ? new S; +return !(!s || !arg) ? 1 : 0; +"""); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("0", Eval2.eval(code, 0)); + assertEquals("1", Eval2.eval(code, 1)); + } + + @Test + public void testAndPtr() throws IOException { + // Todo: have one src here + String src = """ +struct S { !S? !fld; new S = { !S? f -> fld=f; }; }; +val ptr = arg == 1 ? null : new S(arg==1 ? null : new S(null)); +return ptr && ptr.fld ? "true" : "false"; +"""; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("true", Eval2.eval(code, 0)); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src, "class:Test.<clinit>", 1, 12, false); + int trap = R5.step(100); + assertEquals(0,trap); + int adr = (int)R5.regs[riscv.A0]; // Returns a Simple *u8[~] string + assertEquals(4,R5.ld4s(adr)); // String length + assertEquals(0x65757274,R5.ld4s(adr+4)); // "true" + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("class:Test.<clinit>", src, 1, 12, false); + trap = arm.step(100); + assertEquals(0,trap); + int adr2 = (int)arm.regs[0]; // Returns a Simple *u8[~] string + assertEquals(4,arm.ld4s(adr2)); // String length + assertEquals(0x65757274,arm.ld4s(adr2+4)); // "true" + } + + // conditional side effect + @Test + public void testCondSideEffAnd() throws IOException { + String src = """ +int x=1; +int y=1; +int z=0; + +int g = x++ && y++ && z++; +return x == 2 && y == 2 && z == 1 && g == 0 + ? 0 // Effected, Expected + : 1; // Error, not effected +"""; + + TestC.runSF( src, "and2", "", 0); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src, "and2", 0, 0, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(0,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("and2", src, 0, 0, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(0,arm.regs[0]); + } + + @Test + public void testCondSideEffOr() throws IOException { + String src = """ +int x = -1; +int y = 1; +int z = -1; +int g = x++ || y++ || z++; +int switch = (x == 4 || y == 4 || z == 4) ? -1 : 1; +return (x == 0 && y == 1 && z==-1) && switch && g + ? 0 // Effected, expected + : 1; // Error +"""; + TestC.runSF( src, "or2", "", 0); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src, "or2", 0, 0, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(0,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("or2", src, 0, 0, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(0,arm.regs[0]); + } + + // test it with function calls + @Test + public void testFuncCall() throws IOException { + String src = """ +int a = 1; +int b = 1; +var sq_noInline = { int x -> + x*x; +}; +return (a && sq_noInline(0)) + ? 1 // Error + : 0; // Expected +"""; + TestC.runSF( src, "and3", "", 2); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src, "and3", 0, 2, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(0,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("and3", src, 0, 2, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(0,arm.regs[0]); + } + + + @Test + public void testFRefFields() { + String src = """ +// A hypothetical scanner class +struct _Scan { + int !x; + u8[~] !buf; + new _Scan = { u8[~] b -> buf=b; }; + // Skip whitespace + val skip = { -> + while( buf[x] <= ' ' ) + x++; + }; + // Peek a character; if matched consume it, else false. + val peek = { u8 c -> + skip(); + if( buf[x] != c ) return false; + x++; + return true; + }; +}; +!_Scan !_s = new _Scan(" q"); +return _s.peek('q'); +"""; + + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("return Phi(Region,0,1);", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + testCPU(src,"x86_64_v2", "win64",23,null); + }; + + @Test + public void testFRefFields2() { + String src =""" +// A hypothetical scanner class +struct _Scan { + int !x; + u8[~] !buf; + new _Scan = { u8[~] b -> buf=b; }; + // Skip whitespace + val skip = { -> + while( buf[x] <= ' ' ) + x++; + return self; + }; +}; +~_Scan s = new _Scan(" q"); +return s.skip().x; +"""; + + try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { + // Bad error message, but basically requires a mutable 'x' field. + org.junit.Assert.assertTrue(e.getMessage().startsWith("Argument #0 isa")); + } + }; + + + @Test + public void testMethod() { + String src = """ +// A hypothetical scanner class +struct _Scan { + int !x; + u8[~] !buf; + new _Scan = { u8[~] b -> buf=b; }; + // Skip whitespace + val skip = { -> + while( buf[x] <= ' ' ) + x++; + }; + val require = { u8 ch -> + skip(); + buf[x++]==ch; + }; +}; +!_Scan !_s = new _Scan(" [1,2]"); +return _s.require('['); +"""; + + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("return (.[]==91);", code.print()); + assertEquals("1", Eval2.eval(code, 0)); + testCPU(src,"x86_64_v2", "win64",4, null); + }; + + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter24Test.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter24Test.java new file mode 100644 index 00000000..2bc2ce7f --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Chapter24Test.java @@ -0,0 +1,293 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.isa.eval.EvalRisc5; + +import com.seaofnodes.isa.eval.EvalArm64; + +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.cpus.riscv.riscv; +import java.io.IOException; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.*; + +public class Chapter24Test { + @Test public void testRecursiveWidening() { + String[] srcs = { + // Parm(x) must widen even though the recursive result is constant. + "val f = { int x, int limit -> x >= limit ? 0 : f(x+1,limit); }; return f(0,arg);", + // The growing result has its own cycle, which never crosses Parm(x). + "val f = { int x -> x <= 0 ? 0 : f(x-1)+1; }; return f(3);" + }; + for( int i=0; i<srcs.length; i++ ) + for( long seed : new long[]{0,42,126} ) { + var code = new CodeGen(srcs[i],seed,true).driver(CodeGen.Phase.LocalSched); + assertEquals(i==0 ? "0" : "3",Eval2.eval(code,3)); + } + } + + @Test public void testColdLoopSelfConflict() { com.seaofnodes.simple.codegen.RegAllocTestSupport.coldLoopSelfConflict(); } + + @Test public void testPrintingDeadFunction() throws Exception { + var code = new CodeGen("return 0;").parse(); + var tfp = TypeFunPtr.TEST; + var fun = new FunNode(null,tfp,"deadPrinterTarget",null); + org.junit.Assert.assertTrue(fun.isDead()); + code.link(fun); + var con = ConstantNode.raw(tfp); + var call = new CallNode(null,con,con,con); + con.toString(); + call.name(); + code.funcName(tfp.fidx()); + org.junit.Assert.assertSame(fun,code._link(tfp)); + // Optimizer lookup still performs its usual lazy cleanup. + org.junit.Assert.assertNull(code.link(tfp)); + org.junit.Assert.assertNull(code._link(tfp)); + } + + + @Ignore + @Test + public void testJig() throws IOException { + String src = "int v0=0; v0 = arg && 0== 0 !=0;"; + testCPU(src,"x86_64_v2", "win64" ,-1,null); + testCPU(src,"riscv" , "SystemV",-1,null); + testCPU(src,"arm" , "SystemV",-1,null); + } + + static CodeGen testCPU( String src, String cpu, String os, int spills, String stop ) { + CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); + if( stop != null ) + assertEquals(stop, code.print()); + return code; + } + + // test it with function calls + @Test + public void testStack1() throws IOException { + String src =""" +int something_specific = 11; +return (10 < something_specific < 20) + ? 0 // Expected + : 1; // Error +"""; + TestC.runSF( src, "stacked_r_1", "", 0); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src, "stacked_r_1", 0, 0, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(0,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("stacked_r_1", src, 0, 0, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(0,arm.regs[0]); + } + + @Test + public void testStack3() throws IOException { + String src =""" +var sq_noInline = { int x -> + x*x; +}; +int score = 4; +return (sq_noInline(2) <= score < sq_noInline(3)) + ? 0 // Expected + : 1; // Error +"""; + TestC.runSF( src, "stacked_r_3", "", 2); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src, "stacked_r_3", 0, 2, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(0,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("stacked_r_3", src, 0, 3, false); + int trap_1 = arm.step(100); + assertEquals(0,trap_1); + assertEquals(0,arm.regs[0]); + } + + @Test + public void testStack4() throws IOException { + String src =""" +int something_specific = 9; +return (10 < something_specific < 20) + ? 1 // Error + : 0; // Expected +"""; + + TestC.runSF( src, "stacked_r_4", "", 0); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src, "stacked_r_4", 0, 0, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(0,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("stacked_r_4", src, 0, 0, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(0,arm.regs[0]); + } + + @Test + public void testStack6() { + try { new CodeGen("return 0 < arg > 1;" ).parse(); fail(); } + catch( RuntimeException e ) { assertEquals("Mixing relational directions in a chained relational test",e.getMessage()); } + } + + @Test + public void testStack9() throws IOException { + String src = "var stack9 = { int x -> (x < 1) == 1; };"; + String src2 = "return (0 < 1) == 1;"; + TestC.runC(src, "stacked_r_9x", "1", 0); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src2, "stacked_r_9", 0, 0, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(1,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("stacked_r_9", src2, 0, 0, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(1,arm.regs[0]); + } + + @Test + public void testStack10() throws IOException { + String src ="var stack10 = { int x -> (x < 1) == (1>x);};"; + String src2 ="return (0 < 1) == (1>0);"; + TestC.runC(src, "stacked_r_10x", "1",0); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src2, "stacked_r_10", 0, 0, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(1,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("stacked_r_10", src2,0, 0, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(1,arm.regs[0]); + } + + @Test + public void testStack11() throws IOException { + String src = "var stack11 = { int x -> 0 < x < x+1 < 4;}; "; + String src2 = "return 0 < arg < arg+1 < 4;"; + TestC.runC(src, "stacked_r_11x", "1", 2); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src2, "stacked_r_11", 1, 0, false); + R5.regs[riscv.A1] = 1; // Arg in A1, Test.class in A0 + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(1,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("stacked_r_11", src2, 1, 0, false); + arm.regs[1] = 1; // Arg in r1, Test.class in r0 + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(1,arm.regs[0]); + } + + // shows that it is parsed as + // ((0 != x) != 2) + @Test + public void testStack12() throws IOException { + String src = "var stack12 = { int x -> 0 != x != 2; };"; + String src2 = "return 0 != arg != 2;"; + TestC.runC(src, "stacked_r_12x", "1",0); + + // Evaluate on RISC5 emulator + EvalRisc5 R5 = TestRisc5.build( src2, "stacked_r_12", 1, 0, false); + int trap = R5.step(100); + assertEquals(0,trap); + assertEquals(1,R5.regs[riscv.A0]); + + // Evaluate on ARM emulator + EvalArm64 arm = TestArm64.build("stacked_r_12", src2, 1, 0, false); + trap = arm.step(100); + assertEquals(0,trap); + assertEquals(1,arm.regs[0]); + } + + + // shows that it is parsed as + // ((0 == x) == 1) + @Test + public void testStack13() throws IOException { + String src = "var stack13 = { int x -> 0 == x == 1; };"; + String src2 = "return 0 == arg == 1;"; + TestC.runC(src, "stacked_r_13x", "1",0); + + // Compile once per target, then check false, true and negative inputs. + EvalRisc5 R5 = TestRisc5.build( src2, "stacked_r_13", 1, 0, false); + for( int arg : new int[]{-1,0,1} ) { + R5._pc = 0; + R5.regs[riscv.A1] = arg; + assertEquals(0,R5.step(100)); + assertEquals(arg==0 ? 1 : 0,R5.regs[riscv.A0]); + } + EvalArm64 arm = TestArm64.build("stacked_r_13", src2, 1, 0, false); + for( int arg : new int[]{-1,0,1} ) { + arm._pc = 0; + arm.regs[1] = arg; + assertEquals(0,arm.step(100)); + assertEquals(arg==0 ? 1 : 0,arm.regs[0]); + } + } + + @Test + public void testSCCP0() { + String src = +""" +int x = 1; +int cnt = 5; +while (cnt) { + cnt -= 1; + if (x == 0) { + x += 1; + } +} +return x; +"""; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("return 1;", code.print()); + } + + @Test + public void testSCCP1() { + String src = +""" +int x = 1; +int cnt = 5; +while( cnt-- ) + if( x == 1 ) + x = 2-x; +return x; +"""; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("return 1;", code.print()); + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java new file mode 100644 index 00000000..6b4a987d --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java @@ -0,0 +1,435 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.type.Field; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeTuple; +import com.seaofnodes.simple.util.Ary; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runners.model.Statement; +import static org.junit.Assert.*; + +public class ClassInitDependenciesTest { + private final ArrayList<Path> dirs = new ArrayList<>(); + + @Rule public final TestRule cleanup = (test, description) -> new Statement() { + @Override public void evaluate() throws Throwable { + // Keep all fixtures if compilation or any later assertion fails. + test.evaluate(); + for( Path dir : dirs ) { + Path[] files; + try( var paths=Files.walk(dir) ) { + files=paths.sorted(Comparator.reverseOrder()).toArray(Path[]::new); + } + for( Path file : files ) Files.delete(file); + } + } + }; + + private Path tempDir(String prefix) throws Exception { + Path build=Path.of("build/init-deps"); + Files.createDirectories(build); + Path dir=Files.createTempDirectory(build,prefix); + dirs.add(dir); + return dir; + } + + private CodeGen compile(String entry, Map<String,String> sources, long seed) throws Exception { + Path root=tempDir("case-"); + Files.writeString(root.resolve("Root.smp"),"val entry="+entry+";"); + for( var source : sources.entrySet() ) { + Path path=root.resolve("Root/"+source.getKey()+".smp"); + Files.createDirectories(path.getParent()); + Files.writeString(path,source.getValue()); + } + return new CodeGen(root.toString(),root.resolve("obj").toString(), + "Root",null,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + } + + private void cycle(String entry, Map<String,String> sources, String expected) throws Exception { + expected="Root."+expected.replace(" -> "," -> Root."); + for( long seed : new long[]{1,123,987654321} ) { + try { compile(entry,sources,seed); fail("Expected initialization cycle"); } + catch( Parser.ParseException ex ) { + assertCycle(ex.getMessage(),expected); + } + } + } + + // The report lists internal SCC edges, including ordinary helper calls. + private HashMap<String,HashSet<String>> reportEdges(String message) { + String[] lines=message.split("\n"); + assertEquals(message,"Cyclic class initialization:",lines[0]); + HashMap<String,HashSet<String>> edges=new HashMap<>(); + for( int i=1; i<lines.length; i++ ) { + if( !lines[i].startsWith(" ") || lines[i].startsWith(" ") ) continue; // Diagnostic/source/caret. + String[] edge=lines[i].trim().split(" -> "); + assertEquals(message,2,edge.length); + edges.computeIfAbsent(edge[0],key -> new HashSet<>()).add(edge[1]); + } + return edges; + } + + // Follow helpers in the diagnostic, stopping at the next class initializer. + private boolean reportedTouch(HashMap<String,HashSet<String>> edges, String from, String to, HashSet<String> seen) { + if( !seen.add(from) || !edges.containsKey(from) ) return false; + for( String dst : edges.get(from) ) + if( dst.equals(to) || (dst.contains(" [fidx ") && reportedTouch(edges,dst,to,seen)) ) return true; + return false; + } + + private void assertCycle(String message, String expected) { + HashMap<String,HashSet<String>> edges=reportEdges(message); + String[] path=expected.split(" -> "); + for( int i=1; i<path.length; i++ ) + assertTrue("Missing "+path[i-1]+" -> "+path[i]+" in:\n"+message, + reportedTouch(edges,path[i-1],path[i],new HashSet<>())); + } + + // Follow ordinary helpers, stopping at the next initializer. This checks + // the class prerequisite relation without requiring a total init order. + private boolean touches(CodeGen code, ClassInitDependencies deps, String from, String to) { + int src=code.link(code._compunits.get(from)).sig().fidx(); + int dst=code.link(code._compunits.get(to)).sig().fidx(); + return touches(code,deps._edgess,src,dst,new BitSet()); + } + + private boolean touches(CodeGen code, int[][] edges, int src, int dst, BitSet seen) { + if( src>=edges.length || seen.get(src) || edges[src]==null ) return false; + seen.set(src); + for( int edge : edges[src] ) { + if( edge==dst ) return true; + FunNode fun=code.link(edge); + if( fun!=null && !fun.isClz() && touches(code,edges,edge,dst,seen) ) return true; + } + return false; + } + + @Test public void testEdgesAndPassiveReferences() throws Exception { + CodeGen code = compile("A",Map.of("A","int value=B.value;", "B","int value=arg|1;"),123); + ClassInitDependencies deps=new ClassInitDependencies(code); + assertTrue(touches(code,deps,"Root/A","Root/B")); + code=compile("A",Map.of("A","val b=B;", "B","val a=A;"),123); + deps=new ClassInitDependencies(code); + assertFalse(touches(code,deps,"Root/A","Root/B")); + assertFalse(touches(code,deps,"Root/B","Root/A")); + assertTrue(touches(code,deps,"Root/A","Root")); + assertTrue(touches(code,deps,"Root/B","Root")); + } + + @Test public void testCycles() throws Exception { + cycle("A",Map.of("A","val get_noInline={ ->\n B.value;\n};\nint value=get_noInline();", + "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","int value=B.value;", "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","val get={ -> B.value; }; int value=get();", + "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","int value=get(); val get={ -> B.value; };", + "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","int value=0; if(arg) value=B.value;", + "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","val f=arg ? { -> B.value; } : { -> 0; }; int value=f();", + "B","int value=A.value;"),"A -> B -> A"); + cycle("P",Map.of("P","int value=A.value;", "P/A","int value=arg;"),"P -> P.A -> P"); + } + + @Test public void testSiblingsAndNonreturningCall() throws Exception { + cycle("P",Map.of("P","val a=A; val b=B;", "P/A","int value=B.value;", + "P/B","int value=A.value;"),"P.A -> P.B -> P.A"); + cycle("A",Map.of("A","val get={ -> val x=B.value; while(1) {} return x; }; int value=get();", + "B","int value=A.value;"),"A -> B -> A"); + // An out-of-line nonreturning call retains its continuation's touches. + cycle("A",Map.of( + "A","int value=1; val stop_noInline={ -> while(1) {} return 1; }; stop_noInline(); return B.value;", + "B","int value=A.value;"),"A -> B -> A"); + String source="val stop_noInline={ -> while(1) {} return 1; }; stop_noInline(); return 2;"; + CodeGen code=new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + assertNull(Eval2.eval(code,0,10)); // Evaluation reaches its loop budget. + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) + new CodeGen(source).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + + @Test public void testOptimizedDependencies() throws Exception { + for( long seed : new long[]{1,123,987654321} ) { + // SCCP learns this condition from a call; the parser cannot decide it. + CodeGen code=compile("A",Map.of( + "A","val test_noInline={ -> false; }; int value=1; if(test_noInline()) value=B.value;", + "B","int value=A.value;"),seed); + ClassInitDependencies deps=new ClassInitDependencies(code); + assertFalse(touches(code,deps,"Root/A","Root/B")); + assertTrue(touches(code,deps,"Root/B","Root/A")); + + code=compile("A",Map.of( + "A","val test_noInline={ -> false; }; val f=test_noInline() ? { -> B.value; } : { -> 1; }; int value=f();", + "B","int value=A.value;"),seed); + assertFalse(touches(code,new ClassInitDependencies(code),"Root/A","Root/B")); + + // An unused helper's body must not become an initializer dependency. + code=compile("A",Map.of("A","val unused={ -> B.value; }; int value=1;", + "B","int value=A.value;"),seed); + assertFalse(touches(code,new ClassInitDependencies(code),"Root/A","Root/B")); + } + // Discarding the value of a field read cannot discard its active touch. + cycle("A",Map.of("A","int value=1; B.value; return 0;", + "B","int value=2; A.value; return 0;"),"A -> B -> A"); + } + + @Test public void testRecursionAndNativeCalls() throws Exception { + CodeGen code=compile("A",Map.of("A","val f={int n -> n ? f(n-1) : B.value; }; int value=f(arg);", + "B","int value=arg|1;"),123); + assertTrue(touches(code,new ClassInitDependencies(code),"Root/A","Root/B")); + code=compile("A",Map.of("A","{int} native=\"C\"; return native();"),123); + + } + + private void object(Path dir, Path unused, String name, String source) throws Exception { + Path file=dir.resolve("Root/"+name+".smp"); + Files.createDirectories(file.getParent()); + Files.writeString(file,source); + } + + private void module(Path dir, Path unused, String name, Map<String,String> sources) throws Exception { + for( var entry : sources.entrySet() ) object(dir,dir,entry.getKey(),entry.getValue()); + } + + private CodeGen entry(Path dir, String source, long seed) throws Exception { + Files.writeString(dir.resolve("Root.smp"),""); + object(dir,dir,"Main",source); + return new CodeGen(dir.toString(),dir.toString(),"Root/Main",null,seed,true,TypeInteger.BOT); + } + + private CodeGen emit(Path dir, String name, String source) throws Exception { + return entry(dir,source,123).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + } + + @Test public void testCycleReportDirection() throws Exception { + cycle("A",Map.of("A","int value=B.value;", "B","int value=C.value;", + "C","int value=A.value;"),"A -> B -> C -> A"); + for( long seed : new long[]{1,123,987654321} ) { + // Stack order A,B,C is not a cycle: B has no edge to C. All four + // internal edges must be shown; edges to Root/D must be omitted. + try { + compile("A",Map.of("A","int value=B.value+C.value+D.value;", + "B","int value=A.value;", "C","int value=A.value;", + "D","int value=arg;"),seed); + fail("Expected initialization cycle"); + } catch( Parser.ParseException ex ) { + assertEquals(Map.of("Root.A",Set.of("Root.B","Root.C"), + "Root.B",Set.of("Root.A"), "Root.C",Set.of("Root.A")), + reportEdges(ex.getMessage())); + } + } + } + + @Test public void testCycleSourceLocations() throws Exception { + for( String name : new String[]{"read", "read_noInline"} ) { + try { + compile("A",Map.of("A","int value=Helper."+name+"();", + "Helper","val "+name+"={ ->\n B.value;\n};", + "B","int value=A.value;"),123); + fail("Expected initialization cycle"); + } catch( Parser.ParseException ex ) { + String message=ex.getMessage(); + assertCycle(message,"Root.A -> Root.B -> Root.A"); + // Inlining must keep Helper's source identity, not substitute A. + assertTrue(message,message.contains("Helper.smp:2:4")); + assertTrue(message,message.contains(" B.value;\n ---^")); + if( name.endsWith("noInline") ) { + assertTrue(message,message.contains("A.smp:1:31")); + assertTrue(message,message.contains("int value=Helper.read_noInline();")); + } + } + } + } + + @Test public void testSourceError() { + String src="// π\r\n\treturn 1;\r\n"; + CodeGen code=new CodeGen(src).driver(CodeGen.Phase.Parse); + CompUnit cu=code._compunits.get("Test"); + int pos=src.substring(0,src.indexOf('1')).getBytes(java.nio.charset.StandardCharsets.UTF_8).length; + assertEquals("Test.smp:2:9: error: example\n return 1;\n ---------------^",SourceError.format(cu,pos,"example")); + assertEquals("Test.smp:3:1: error: example\n \n ^",SourceError.format(cu,src.getBytes(java.nio.charset.StandardCharsets.UTF_8).length,"example")); + } + + @Test public void testSelfInitialization() throws Exception { + for( String name : new String[]{"read","read_noInline"} ) + for( long seed : new long[]{1,123,987654321} ) + compile("A",Map.of("A","int value=arg; val "+name+"={ -> A.value; }; int result="+name+"();"),seed); + } + + @Test public void testInitializationMemory() throws Exception { + String[] sources={ + "int value=B.value+B.value;", + "int value; if(arg) value=B.value; else value=B.value+1; value=value+B.value;", + "int value; if(arg) value=B.value; value=value+B.value;" + }; + int[] counts={1,2,2}; + for( long seed : new long[]{1,123,987654321} ) + for( int i=0; i<sources.length; i++ ) { + CodeGen code=compile("A",Map.of("A",sources[i],"B","int value=arg;"),seed); + FunNode fun=code.link(code._compunits.get("Root/A")); + CompUnit b=code._compunits.get("Root/B"); + int[] touches={0}; + code._stop.walk(n -> { + if( n instanceof CLInitNode init && init.fun()==fun && init.cu()==b ) { + touches[0]++; + TypeTuple state=(TypeTuple)init._type; + assertSame(TypeMem.BOT,state._types[1]); + TypeMem flag=(TypeMem)state._types[2]; + assertEquals(b._clz.field("$init")._alias,flag._alias); + assertSame(TypeInteger.TRUE,flag._t); + assertNotNull(init.ready()); + } + return null; + }); + assertEquals(sources[i],counts[i],touches[0]); + } + } + + @Test public void testForwardInstanceTouch() { + String src=""" + struct S { int x=7; }; + val _get={ -> _make().x; }; + val _make={ -> new S; }; + return _get(); + """; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.Parse); + CLInitNode init=code._stop.walk(n -> n instanceof CLInitNode c ? c : null); + assertNotNull("The unresolved receiver needs a speculative check",init); + assertNotNull("The readiness load is present from parsing",init.ready()); + code.driver(CodeGen.Phase.TypeCheck); + assertNull("An instance needs neither CLInit nor its speculative $init load", + code._stop.walk(n -> n instanceof CLInitNode || + (n instanceof LoadNode ld && ld._name.equals("$init")) ? n : null)); + assertEquals("7",Eval2.eval(code,0)); + } + } + + @Test public void testInitializerFlagStore() throws Exception { + // A same-named struct must retain the file class's hidden flag. + CodeGen code=compile("A",Map.of("A","struct A { int value; }; int value=arg;"),123); + CompUnit a=code._compunits.get("Root/A"); + Field flag=a._clz.field("$init"); + assertNotNull(flag); + assertFalse(flag._final); + FunNode fun=code.link(a); + int[] stores={0}; + code._stop.walk(n -> { + if( n instanceof StoreNode st && st._name.equals("$init") && st._alias==flag._alias ) { + stores[0]++; + assertSame(TypeInteger.TRUE,st.val()._type); + assertSame(fun,CFGNode.earlyCFG(st).fun()); + } + return null; + }); + assertEquals(1,stores[0]); + } + + @Test public void testInitializationExecution() throws Exception { + Path dir=tempDir("execution-"); + object(dir,dir,"State","int count=0;"); + object(dir,dir,"B","State.count++; int value=17+arg;"); + String src="int value=arg ? B.value+B.value : 0; return value+100*State.count;"; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=entry(dir,src,seed).driver(CodeGen.Phase.TypeCheck); + assertNotNull(code._stop.walk(n -> n instanceof CLInitNode ? n : null)); + assertEquals("0",Eval2.eval(code,0)); + assertNull(code._stop.walk(n -> n instanceof CLInitNode ? n : null)); + assertEquals("134",Eval2.eval(code,1)); + } + + emit(dir,"Main","val run={int take -> "+src.replace("arg ?","take ?")+" };"); + Path cfile=dir.resolve("check.c"); + Files.writeString(cfile,""" + #include <stdint.h> + __attribute__((CALL_CONV)) extern int64_t run(int64_t take) __asm__("Root.Main.run"); + int main(void) { + return run(0)!=0 || run(1)!=134 || run(1)!=134 || run(0)!=100; + } + """); + String exe=dir.resolve(TestC.OS.startsWith("Windows") ? "check.exe" : "check").toString(); + TestC.linkExe(dir.resolve("Root/Main.o").toString(),TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", + cfile.toString(),null,exe); + assertEquals("",TestC.exec(exe)); + for( String cpu : new String[]{"arm","riscv"} ) + entry(dir,src,123).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + + @Test public void testParentInitializationExecution() throws Exception { + Path dir=tempDir("execution-"); + object(dir,dir,"State","int count=0;"); + module(dir,dir,"P",Map.of("P","val C=C; State.count=State.count*10+1;", + "P/C","State.count=State.count*10+2; int value=arg;")); + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=entry(dir,"return P.C.value+State.count;",seed).driver(CodeGen.Phase.TypeCheck); + assertEquals("12",Eval2.eval(code,99)); + } + emit(dir,"Main","val run={ -> P.C.value+State.count; };"); + Path cfile=dir.resolve("check.c"); + Files.writeString(cfile,""" + #include <stdint.h> + __attribute__((CALL_CONV)) extern int64_t run(void) __asm__("Root.Main.run"); + int main(void) { return run()!=12 || run()!=12; } + """); + String exe=dir.resolve(TestC.OS.startsWith("Windows") ? "check.exe" : "check").toString(); + TestC.linkExe(dir.resolve("Root/Main.o").toString(),TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", + cfile.toString(),null,exe); + assertEquals("",TestC.exec(exe)); + } + + @Test public void testNonreturningObject() throws Exception { + Path dir=tempDir("nonreturning-"); + object(dir,dir,"Spin","val spin_noInline={ -> while(1) {} return 7; };"); + CodeGen code=entry(dir,"Spin.spin_noInline(); return 7;",123).driver(CodeGen.Phase.TypeCheck); + assertNull(Eval2.eval(code,0,10)); + } + + + @Test public void testOverlappingFunctionAndClassCycles() throws Exception { + // Fixed function graph: Opto/inlining must not erase the overlap that + // exercises SCC traversal. One clinit is legal, two in the SCC are not. + CodeGen code=compile("A",Map.of("A","val b=B; int value=arg;", "B","int value=arg;"),123); + int f=code._linker._len, g=f+1, a=f+2, b=f+3; + String[] names={"f", "g", "A.<clinit>", "B.<clinit>"}; + for( int i=0; i<names.length; i++ ) { + TypeFunPtr sig=TypeFunPtr.make1((byte)2,false,new Type[0],TypeInteger.BOT,i+f); + CompUnit cu=code._compunits.get(i==3 ? "Root/B" : "Root/A"); + FunNode fun=new FunNode(null,sig,names[i],cu); + fun._type=Type.CONTROL; + code._linker.setX(i+f,fun); + } + ClassInitDependencies deps=new ClassInitDependencies(code); + deps._edgess[f]=new int[]{g}; // f -> g + deps._edgess[g]=new int[]{f}; // g -> f + assertNull("Ordinary mutual recursion is legal",deps.check()); + deps._edgess[a]=new int[]{a}; // A -> A + assertNull("A singleton initializer self-edge is legal",deps.check()); + deps._edgess[a]=new int[]{g}; // A -> g + deps._edgess[f]=new int[]{g,a}; + assertNull("A cycle containing only one initializer is legal",deps.check()); + deps._edgess[a]=new int[]{b}; // A -> B + deps._edgess[b]=new int[]{g}; // B -> g -> f -> A + String forward=deps.check(); + deps._edgess[f]=new int[]{a,g}; + String reverse=deps.check(); + assertTrue("Both successor orders must reject the class cycle: "+forward+", "+reverse, + forward!=null && reverse!=null); + assertCycle(forward,"Root.A -> Root.B -> Root.A"); + assertCycle(reverse,"Root.A -> Root.B -> Root.A"); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java new file mode 100644 index 00000000..e2d822d0 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java @@ -0,0 +1,94 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CompoundAssignmentTest { + private static final String[] OPS = {"&=", "|=", "^=", "<<=", ">>=", ">>>="}; + + private static long apply(String op, long lhs, long rhs) { + return switch (op) { + case "&=" -> lhs & rhs; + case "|=" -> lhs | rhs; + case "^=" -> lhs ^ rhs; + case "<<=" -> lhs << rhs; + case ">>=" -> lhs >> rhs; + case ">>>=" -> lhs >>> rhs; + default -> throw new AssertionError(op); + }; + } + + @Test public void testLocals() { + for (String op : OPS) + for (long arg : new long[]{-8, 0, 5, 64}) { + long expected = apply(op, arg, 3); + check("int x=arg; int r=(x " + op + " 3); return x*10+r;", + arg, expected*11); + } + } + + @Test public void testRhsOnce() { + for (String op : OPS) { + long expected = apply(op, 5, 1); + check("int x=arg; int count=0; int r=(x " + op + + " (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11); + } + } + + @Test public void testNarrowingAndChaining() { + check("u8 x=255; x <<= 1; return x;", 0, 254); + check("u8 x=255; x ^= 256; return x;", 0, 255); + check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312); + check("int x=-8; x >>= 65; return x;", 0, -4); + check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3); + } + + @Test public void testExistingOperators() { + check("int x=8; x+=2; x-=1; x*=3; x/=9; return x;", 0, 3); + check("int x=arg; int y=x++; int z=x--; return x*100+y*10+z;", 5, 556); + check("return (arg << 2) + (arg >> 1) + (arg >>> 1);", 8, 40); + check("return arg <= 8;", 8, 1); + check("return arg >= 8;", 8, 1); + } + + @Test public void testRejectedAssignments() { + for (String op : OPS) { + rejected("val x=3; x " + op + " 1; return x;"); + rejected("int x=3; x " + op + " 1.5; return x;"); + } + } + + private static void rejected(String src) { + try { + check(src, 0, 0); + fail("Accepted invalid assignment: " + src); + } catch (RuntimeException e) { + assertTrue("Must report a language error: " + e, e instanceof Parser.ParseException); + assertNotNull(e.getMessage()); + assertFalse("Must report a language error: " + e, e.getMessage().contains("Not yet implemented")); + } + } + + @Test public void testFields() { + for (String op : OPS) { + long expected = apply(op, -8, 3); + check("struct S { int x; }; !S !s=new S; s.x=arg; int r=(s.x " + + op + " 3); return s.x*10+r;", -8, expected*11); + } + } + + @Test public void testArrayAddressAndRhsOnce() { + for (String op : OPS) { + long expected = apply(op, 5, 1); + check("int[] !a=new int[2]; a[0]=arg; int i=0; int count=0; " + + "int r=(a[i++] " + op + " (count=count+1)); " + + "return i*1000+count*100+a[0]*10+r;", 5, 1100+expected*11); + } + } + + private static void check(String src, long arg, long expected) { + var code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals(src, Long.toString(expected), Eval2.eval(code, arg)); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Eval2.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Eval2.java new file mode 100644 index 00000000..366bd7f4 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Eval2.java @@ -0,0 +1,540 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.print.IRPrinter; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +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; + + // Class pointers and initializer self parameters share one object per run. + private static final HashMap<String,Object[]> CLASSES = new HashMap<>(); + + public static String eval( CodeGen code, long arg ) { return eval(code,arg,1000); } + public static String eval( CodeGen code, long arg, int timeout ) { + if( code._start.uctrl()==null ) return ""; // The empty program + SB trace = null; // new SB(); // TRACE, set to null for off, new SB() for on + // Force local scheduling phase + code.driver(CodeGen.Phase.LocalSched); + // Set global, so don't have to pass everywhere + CODE = code; + CLASSES.clear(); + + // 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)); + // For all <clinit> / Clazzes, install their uninitialized memory + for( FunNode fun : code._linker ) + if( fun!=null && fun.isClz() ) { + TypeStruct clz = ((TypeMemPtr)fun.sig().arg(0))._obj; + // Map from the <clinit> self parameter to the + // singleton class structure. + F.put(fun.parm(2),classObject(clz)); + } + + + // ------------ + // Evaluate until exit or timeout. Each outer loop step computes + // all data nodes under some new Control. + while( true ) { + if( BB==null ) return 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 NeverNode never: BB = never.cproj(0); break; // Synthetic exit is never taken. + 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(); + } + if( BB==null ) return null; + 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 isClz = r instanceof FunNode fun && fun.isClz(); + + // Parallel assign Phis. First parallel read and cache + int i; + for( i = 0; i < r.nOuts(); i++ ) { + // Due to scheduling, all Phis are first in the outputs; first + // non-Phi is the end of all Phis. + if( !(r.out(i) instanceof PhiNode phi) ) break; + + // Parms read from Call args - + // - except RPC is the actual prior frame + // - except entry <clinit> takes the external program argument + Object val; + if( isClz && prior instanceof StartNode && phi instanceof ParmNode parm && parm._idx==3 ) + PHICACHE[i] = arg; // Reading the initial arguments to <clinit> + else { + Node n = phi instanceof ParmNode parm + // RPC uses the prior 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); + // Read the frame before overwriting + 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; the Phis have been + // evaluated here, and now we need to evaluate the rest of the block + 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 AddNode add -> add.mode()==1 + ? (Object)(x(add.in(1)) + x(add.in(2))) + : (Object)(d(add.in(1)) + d(add.in(2))); + case AndNode and -> x(and.in(1)) & x(and.in(2)); + 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 -> le.mode()==1 + ? (x(le.in(1)) <= x(le.in(2)) ? 1L : 0L) + : (d(le.in(1)) <= d(le.in(2)) ? 1L : 0L); + case BoolNode.LT lt -> lt.mode()==1 + ? (x(lt.in(1)) < x(lt.in(2)) ? 1L : 0L) + : (d(lt.in(1)) < d(lt.in(2)) ? 1L : 0L); + case GuardNode guard -> val(guard.in(1)); + case ConstantNode con -> con(con._con); + case DivNode div -> div.mode()==1 + ? (Object)(x(div.in(2))==0 ? 0L : x(div.in(1)) / x(div.in(2))) + : (Object)((d(div.in(2))==0 ? 0D : d(div.in(1))) / d(div.in(2))); + case EscapeNode esc -> esc(esc); + case FunPtrNode fptr -> con(fptr._con); + case LoadNode ld -> load(ld); + case MemMergeNode merge-> "$mem"; + case MinusNode sub -> sub.mode()==1 ? (Object)(- x(sub.in(1))) : (Object)(- d(sub.in(1))); + case MulNode mul -> mul.mode()==1 + ? (Object)(x(mul.in(1)) * x(mul.in(2))) + : (Object)(d(mul.in(1)) * d(mul.in(2))); + case NewNode alloc-> alloc(alloc); + case NotNode not -> not(not); + 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 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 -> sub.mode()==1 + ? (Object)(x(sub.in(1)) - x(sub.in(2))) + : (Object)(d(sub.in(1)) - d(sub.in(2))); + case RoundF32Node rnd -> (double)(float)d(rnd.in(1)); + case ToIntegerNode cvt -> (long)d(cvt.in(1)); + 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)d; } + // Fetch and unbox as primitive double + static double d( Node n ) { Object d = F.get(n); return d==null ? 0 : (Double)d; } + // 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 TypeFunPtr tfp -> tfp; + case TypeMem mem -> "MEM"; + case TypeMemPtr tmp -> { + if( tmp._obj.isAry() ) { // Constant array ptr + Type elem = tmp._obj.field("[]")._t; + if( elem instanceof TypeConAry con ) { + Object[] xs = new Object[con.len()]; + for( int i=0; i<con.len(); i++ ) + xs[i] = con.at8(i); + yield xs; + } else { + // Generic constant array, used as a default input to a + // function; should never execute. + yield new Object[0]; + } + + } else if( Parser.startsClzPrefix(tmp._obj._name) ) { + yield classObject(tmp._obj); + } else { + // Constant object with all zeros + yield alloc(tmp._obj); + } + } + default -> null; + }; + } + + // Escapes of constant memory actually needs to set the memory + private static Object esc(EscapeNode esc) { + if( esc.priv() instanceof ConstantNode con ) { + TypeMem mem = (TypeMem)con._type; + Object val = con(mem._t); + TypeMemPtr tmp = (TypeMemPtr)esc.self()._type; + int idx = tmp._obj.findAlias(esc.fld()._alias); + Object[] fs = (Object[])val(esc.self()); + if( tmp._obj.isAry() ) { + assert idx==0; // length field + assert (Long)val==fs.length; + } else { + fs[idx] = val; + } + } + return "$mem"; + } + + + // Convert array size to array element count + private static int offToIdx( long off, TypeStruct t) { + off -= t.aryBase(); + int scale = t.aryScale(); + assert (off & ((1L << scale)-1)) == 0; + return (int)(off>>scale); + } + + // Builds and returns a pointer Object + private static Object[] alloc(NewNode alloc) { + TypeStruct type = alloc._ts; + 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]._t; + 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; + } + + return alloc(type); + } + private static Object[] alloc(TypeStruct type) { + assert !type.isAry(); + int num = type._fields.length; + Object[] ptr = new Object[num]; + for( int i=0; i<num; i++ ) + ptr[i] = con(type._fields[i]._t.makeZero()); + return ptr; + } + + private static Object[] classObject(TypeStruct type) { + Object[] ptr = CLASSES.get(type._name); + if( ptr==null ) CLASSES.put(type._name,ptr=alloc(type)); + return ptr; + } + + private static Object not( NotNode not ) { + Object n = val(not.in(1)); + return ( n==null || (n instanceof Double D && D==0.0) || n instanceof Long L && L==0 ) + ? 1L : 0L; + } + + private static Object load( LoadNode ld ) { + Object f = val(ld.ptr()); + // Check for dense constant array + if( f instanceof TypeMemPtr tmp ) { + throw Utils.TODO(); + } + Object[] fs = (Object[])f; + if( ld._name.equals("#") ) + return (long)fs.length; + TypeMemPtr tmp = (TypeMemPtr)ld.ptr()._type; + 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); + + Object[] xs = (Object[])x; // Array of fields + if( tmp._obj.isAry() ) { + Type elem = tmp._obj._fields[1]._t; + if( elem.isa(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("[ "); + if( elem instanceof TypeConAry ) elem = TypeInteger.ZERO; + 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]._t,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/chapter-modules/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java new file mode 100644 index 00000000..f583e2d2 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java @@ -0,0 +1,65 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.StoreNode; +import org.junit.Test; +import static org.junit.Assert.*; + +public class FinalConstructionTest { + private static final String LOOP = """ + val step={int x -> x*2+1;}; + struct C { + val x=1; + new C={int n -> + x=0; x=1; + for(int i=0; i<n; i++) x=step(x); + }; + val get={->x;}; + }; + return new C(arg).get(); + """; + + @Test public void testFinalConstructorLocals() throws java.io.IOException { + var code=new CodeGen(LOOP).driver(CodeGen.Phase.LocalSched); + assertEquals("1",Eval2.eval(code,0)); + assertEquals("15",Eval2.eval(code,3)); + assertEquals("63",Eval2.eval(code,5)); + code._stop.walk(n -> { + if( n instanceof StoreNode st && st._name.equals("x") ) + assertEquals("Final field writes belong after the loop",0,st.cfg0().loopDepth()); + return null; + }); + String[] sources={ + "struct C { val x=1; new C={->x=1; x=2;}; }; return new C().x;", + "struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i<n;i++) self.x*=2;}; }; return new C(arg).x;", + "struct C { val x=1; new C={int n -> for(int i=0;i<n;i++) ++x;}; }; return new C(arg).x;", + "struct C { val x=1.5; new C={->x=2.5;}; }; return new C().x;", + "struct C { val x=\"a\"; new C={int n -> for(int i=0;i<n;i++) x=\"b\";}; }; return new C(arg).x[0];", + "struct C { val x=new int[1]; new C={->x=new int[2]; x=new int[3];}; }; return new C().x#;" + }; + String[] results={"2","8","4","2.5","98","3"}; + for( int i=0; i<sources.length; i++ ) + assertEquals(results[i],Eval2.eval(new CodeGen(sources[i]).driver(CodeGen.Phase.TypeCheck),3)); + + String[] rejected={ + "struct C { val x=1; }; val c=new C(); c.x=2; return c.x;", + "struct C { new C={->val x=1; x=2;}; }; return new C();", + "struct C { val f={->1;}; new C={->f={->2;};}; }; return new C().f();" + }; + for( String src : rejected ) { + try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(src); } + catch( Parser.ParseException e ) { assertTrue(e.getMessage(),e.getMessage().startsWith("Cannot")); } + } + + var dir=java.nio.file.Path.of("build/objs"); + java.nio.file.Files.createDirectories(dir); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.smp"),"val buildFinal={int arg ->"+LOOP+"};"); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.c"),""" + extern long long buildFinal(long long); + int main(void) { return buildFinal(0)!=1 || buildFinal(3)!=15 || buildFinal(5)!=63; } + """); + TestC.run("val buildFinal={int arg ->"+LOOP+"};","finalConstruction",null,TestC.CALL_CONVENTION, + TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", + dir.resolve("finalConstruction.c").toString(),"",-1); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/FuzzerWrap.java b/chapter-modules/src/test/java/com/seaofnodes/simple/FuzzerWrap.java new file mode 100644 index 00000000..68bbe918 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/FuzzerWrap.java @@ -0,0 +1,78 @@ +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 { + + private static final long[] REGRESSION_SEEDS = { + 375135762521757909L, // bad LCA + -148471672577312953L, // bulk mem + -5037182906211190034L, // dead Guard control + -6359653295501938199L, // monotonicity, escape is dead + 6506797708065910879L, // required field without a user constructor + 650692970082394944L, // recursive bulk mem split + 8335100609598232836L, // ternary result killed by region cleanup + 2520958273643234516L, // nested guard merge + -3772838984504063325L, // fuzzer regression + -4628356252269023530L, // idepth cache version wrap + -4022199781524079249L, // fuzzer regression + 8824787517620928178L, // unknown-mode unary minus during SCCP + -673501011619761901L, // peephole killed self + -8212834489697770130L, // bad keep/unkeep in parser + 478567486335112458L, // dying escape node + -5114548365532699915L, // dominate flags dependencies + -4710931876754133070L, // Tests monotonic phi widening + }; + + private static final long[] OPEN_FAILING_SEEDS = { + }; + + + @Test public void fuzzPeepsRegression () { fuzzPeepsSeeds( REGRESSION_SEEDS); } + @Test @Ignore public void fuzzPeepsOpenFailures() { fuzzPeepsSeeds(OPEN_FAILING_SEEDS); } + + private static void fuzzPeepsSeeds(long... seeds) { + var fuzzer = new Fuzzer(); + for (long seed : seeds) + fuzzer.fuzzPeepsRegression(seed); + } + + @Ignore + @Test + public void fuzzPeepsRandom() { + Random R = new Random(System.currentTimeMillis()); + var fuzzer = new Fuzzer(); + for (int i=0; i<100; i++) + fuzzer.fuzzPeeps(R.nextLong()); + assertTrue(fuzzer.noExceptions()); + } + + + @Ignore + @Test + public void fuzzPeepsLarge() { + var fuzzer = new Fuzzer(); + for (int i=0; i<1000000; i++) + fuzzer.fuzzPeeps(i); + assertTrue(fuzzer.noExceptions()); + } + + @Ignore + @Test + 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/chapter-modules/src/test/java/com/seaofnodes/simple/Main.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Main.java new file mode 100644 index 00000000..1248a6cf --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Main.java @@ -0,0 +1,18 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +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.driver(CodeGen.Phase.FinalizeIR).driver(CodeGen.Phase.Schedule).localSched(); + System.out.println(code._stop); + System.out.println(Eval2.eval(code,arg,100000)); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java new file mode 100644 index 00000000..adeba136 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java @@ -0,0 +1,104 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.*; +import java.util.BitSet; +import org.junit.Test; +import static org.junit.Assert.*; + +// Execution regressions are separate from the fixed spill-measurement cohorts. +public class MemoryEncodingTest { + @Test public void testLoopLoadSearch() { + String[] sources = { + """ + u8[] !program=new u8[106]; u8[] !out=new u8[0]; + int sum=0; + for(int i=0; i<arg; i++) { + sum+=program#; + if(i==1) out=new u8[i+1]; + else if(i==3) out=new u8[2]; + } + return sum+out#; + """, + // One possibly aliasing arm must reject the unchanged-memory proof. + """ + struct S { int x; }; !S !a=new S; !S !b=new S; + a.x=10; b.x=20; !S !p=a; if(arg&1) p=b; + int sum=0; + for(int i=0; i<6; i++) { + if(i==2) p.x=30; + else if(i==4) b.x=40; + sum+=a.x; + } + return sum; + """, + // Unchanged memory does not imply an unchanged loop-carried pointer. + """ + struct S { int x; }; !S !a=new S; !S !b=new S; + a.x=10; b.x=20; !S !p=a; int sum=0; + for(int i=0; i<6; i++) { + if(i==2) a.x=30; + sum+=p.x; + p=b; if(i&1) p=a; + } + return sum; + """, + // All arms fold, but their values differ: build a value Phi. + """ + struct S { int x; }; !S !a=new S; a.x=1; int sum=0; + for(int i=0; i<6; i++) { + sum+=a.x; + if(i&1) a.x=2; + else if(i==2) a.x=3; + else a.x=4; + } + return sum; + """ + }; + // Exercise both unresolved Store aliases and nested value-Phi cleanup. + for( long seed : new long[]{0,9,126} ) + for( int p=0; p<sources.length; p++ ) { + CodeGen code=new CodeGen(sources[p],seed,true).driver(CodeGen.Phase.FinalizeIR); + if( p==0 ) assertNull(code._stop.walk(n -> + n instanceof LoadNode ld && ld.ptr() instanceof ProjNode ptr && + ptr.in(0) instanceof NewNode nn && + nn.size()._type==com.seaofnodes.simple.type.TypeInteger.constant(110) ? ld : null)); + for( int arg=0; arg<7; arg++ ) { + long expected=switch(p) { + case 0 -> 106*arg+(arg>=2 ? 2 : 0); + case 1 -> (arg&1)==0 ? 140 : 60; + case 2 -> 130; + default -> 16; + }; + assertEquals(Long.toString(expected),Eval2.eval(code,arg)); + } + } + } + + @Test public void testKeepReadsBeforeWrites() { + var code = new CodeGen(""" + struct S { int x; int y; }; + !S !a = new S; !S !b = new S; + a.x=arg; b.x=arg+1; + !S !p=a; !S !q=b; + if (arg<0) { p=b; q=a; } + int v; + if (arg>1) { v=p.x; p.x=41; p.y=5; } + else { v=q.x; q.x=42; } + return v; + """).driver(CodeGen.Phase.FinalizeIR); + StopNode stop = code._stop; + assertEquals(2,countMemoryNodes(stop,LoadNode.class,new BitSet())); + assertEquals("-1",Eval2.eval(code,-1)); + assertEquals("1",Eval2.eval(code,0)); + assertEquals("3",Eval2.eval(code,3)); + } + + private static int countMemoryNodes(Node n, Class<?> kind, BitSet seen) { + if (n==null || seen.get(n._nid)) return 0; + seen.set(n._nid); + int cnt=kind.isInstance(n) ? 1 : 0; + for (Node def : n._inputs) cnt+=countMemoryNodes(def,kind,seen); + return cnt; + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/MergeSortTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/MergeSortTest.java new file mode 100644 index 00000000..f5d3d146 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/MergeSortTest.java @@ -0,0 +1,83 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.isa.eval.EvalRisc5; + +import com.seaofnodes.simple.node.cpus.riscv.riscv; +import com.seaofnodes.simple.util.SB; +import java.io.IOException; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class MergeSortTest { + + @Test public void testMergeSort0() throws IOException { + // Test merge sort on a small array of primes + String src = +""" +val merge_sort = { int[] a -> + int[] !b = new int[a#]; + _copy_array (a, 0, a#, b); + _split_merge(a, 0, a#, 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[] primes = new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, }; + SB sb = new SB().p(primes.length).p("["); + for( int prime : primes ) + sb.p(prime).p(", "); + String sprimes = sb.p("]").toString(); + TestC.runC(src, "sort", sprimes, 34); + + + EvalRisc5 R5 = TestRisc5.build( src, "merge_sort", 0, 36, false); + // Allocate an array of primes, filled in reverse order + int ps = R5._heap; + R5._heap += (primes.length+1)*8; // 25 primes, and length + R5.st4(ps,primes.length); + for( int i=0; i<primes.length; i++ ) + R5.st8(ps+(primes.length-1-i+1)*8,primes[i]); + // Pass array base when calling `merge_sort` + R5.regs[riscv.A0] = ps; // Array base to be sorted + + int trap = R5.step(100000); + assertEquals(0,trap); + + assertEquals(primes.length,R5.ld4s(ps)); + for( int i=0; i<primes.length; i++ ) + assertEquals(primes[i], R5.ld8(ps+(i+1)*8)); + + // TODO: replace with ARM Eval + Chapter21Test.testCPU(src,"arm", "SystemV",36,null); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/ModulesTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/ModulesTest.java new file mode 100644 index 00000000..be54b43d --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/ModulesTest.java @@ -0,0 +1,248 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.isa.eval.EvalRisc5; + +import com.seaofnodes.isa.eval.EvalArm64; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.codegen.ParseAll; +import com.seaofnodes.simple.node.CallNode; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.LoopNode; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class ModulesTest { + + @Test public void testFlagSpills() throws IOException { + com.seaofnodes.simple.codegen.FlagSpillTestSupport.run(); + } + + @Ignore + @Test + public void testJig() { + String src = "{ i32 -> i32 } close = \"C\"; val main = { u8[] a -> if (a# > 1) return close(0); return a[0]; };"; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.Encoding,"riscv","SystemV"); + assertEquals("return (arg+3);", code.print()); + assertEquals("3",Eval2.eval(code,0)); + } + + @Test public void testConditionalClassInitX86() throws IOException { + Path build=Path.of("build/objs"); + Files.createDirectories(build); + Path dir=Files.createTempDirectory(build,"init_flags_"); + Files.createDirectories(dir.resolve("Main")); + Files.writeString(dir.resolve("Main/B.smp"),"int value=17+arg;"); + // GCM can hoist the fused B.$init compare above the test of take. + // Its flags then cross another flag definition and need a legal split. + new CheckedCodeGen(dir.toString(),dir.toString(), + "Main","val run={int take -> return take ? B.value : 0; };",123,true,TypeInteger.BOT) + .driver(CodeGen.Phase.Encoding,"x86_64_v2","win64"); + // Remove successful fixtures; preserve the object if compilation fails. + Files.delete(dir.resolve("Main/B.smp")); + Files.delete(dir.resolve("Main")); + Files.delete(dir); + } + + @Test public void testPublicInstanceMethod() { + String src = """ + struct Scan { + val visible = { u8 c -> c==255; }; + val _hidden = { u8 c -> c==1; }; + }; + struct _Private { + val hiddenClassMethod = { u8 c -> c==2; }; + }; + return new Scan.visible(44); + """; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.Parse); + FunNode visible = null; + int checked = 0; + for( FunNode fun : code._linker ) { + if( fun==null || fun._name==null ) continue; + if( fun._name.equals("visible") ) { + assertTrue(code.publicFIDX(fun.sig().fidx())); + visible = fun; + checked++; + } + if( fun._name.equals("_hidden") || fun._name.equals("hiddenClassMethod") ) { + assertFalse(code.publicFIDX(fun.sig().fidx())); + checked++; + } + } + assertEquals(3,checked); + code.driver(CodeGen.Phase.TypeCheck); + assertTrue(visible.unknownCallers()); + assertEquals(TypeInteger.U8,visible.parm(3)._type); + assertEquals("0",Eval2.eval(code,0)); + } + } + + @Test public void testNeverReturnPointerRiscV() { + new CodeGen(""" + struct _S { int x; }; + !_S !s = new _S; + if(arg) while(1) { s.x += arg; } + return s; + """).driver(CodeGen.Phase.Encoding,"riscv","SystemV"); + } + + @Test public void testInlineNeverExit() { + String[] sources={ + "val _spin={ -> while(1) {} return 1; }; _spin(); return 2;", + // Two callers require cloning, including the non-exiting arm. + "val _spin={int x -> if(x) while(1) {} return 7; }; return _spin(arg)+_spin(arg);" + }; + for( int i=0; i<sources.length; i++ ) { + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(sources[i],seed,true).driver(CodeGen.Phase.TypeCheck); + assertNull("Calls should inline",code._start.walk(n -> n instanceof CallNode ? n : null)); + assertNotNull("The non-exiting path must survive",code._start.walk(n -> n instanceof LoopNode && n!=code._start ? n : null)); + if( i==1 ) assertEquals("14",Eval2.eval(code,0,10)); + assertNull(Eval2.eval(code,1,10)); + } + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) + new CheckedCodeGen(sources[i]).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + } + + @Test + public void testExternData() throws IOException { + String src = """ + i32 counter="C"; + i8 small="C"; + f64 fraction="C"; + {int} bump="C"; + val test_global={ -> + val before=counter; + counter=before; + counter=++counter+1; + counter+=1; + val mid=bump(); + small++; + fraction=fraction+0.5; + return before*10000+mid*100+counter; + }; + """; + TestC.runC(src,"extern_data","",-1); + } + + @Test + public void testExternDataEncoding() { + for( String cpu : new String[]{"arm","riscv"} ) { + CodeGen code = new CheckedCodeGen("i32 counter=\"C\"; counter=counter+3; return counter;"); + // Exercise signed low-12 relocation arithmetic, past the first page. + int addr = 0x3800; + code._externDataAddresses.put("counter",addr); + code.driver(cpu,"SystemV",true,false); + byte[] image = new byte[1<<20]; + var bits = code._encoding._bits; + System.arraycopy(bits.buf(),0,image,0,bits.size()); + if( cpu.equals("arm") ) { + EvalArm64 arm = new EvalArm64(image,1<<16); + arm.st4(addr,-7); + assertEquals(0,arm.step(1000)); + assertEquals(-4,arm.regs[0]); + assertEquals(-4,arm.ld4s(addr)); + } else { + EvalRisc5 r5 = new EvalRisc5(image,1<<16); + r5.st4(addr,-7); + assertEquals(0,r5.step(1000)); + assertEquals(-4,r5.regs[10]); + assertEquals(-4,r5.ld4s(addr)); + } + } + } + + + @Test public void testPrintingConstantPool() throws Exception { + com.seaofnodes.simple.codegen.PrintRegTestSupport.checkConstantPool(); + } + + + @Test + public void testForwardConstructor() { + CodeGen code = new CodeGen("src/test/java/com/seaofnodes/simple/test_smp/forward_ctor", + "build/objs/forward_ctor_parse", + "m",null,123L,true,TypeInteger.BOT); + code.driver(CodeGen.Phase.TypeCheck); + } + + + @Test + public void testPostfixFieldUpdate() { + String src = """ + struct V { + u32 !len = 0; + i64[] !buf; + new V = { i64[] b -> buf=b; }; + val grow = { i64 n -> self; }; + val add = { i64 x -> grow(1).buf[len++] = x; self; }; + }; + return new V(new i64[1]).add(7).len; + """; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } + + @Test + public void testStringEscapes() { + String src = "u8[~] s=\"A\\n\\t\\r\\\\\\\"B\"; " + + "return s#==7 && s[0]=='A' && s[1]==10 && s[2]==9 && s[3]==13 && " + + "s[4]==92 && s[5]==34 && s[6]=='B';"; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } + + @Test + public void testCharacterEscapes() { + CodeGen code = new CodeGen("return '\\n'==10 && '\\t'==9 && '\\r'==13;") + .driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } + + @Test + public void testFuzzerUnresolvedNumericLoop() { + CodeGen code = new CodeGen(""" + arg=0; + while(arg+0) + while(-arg) + arg=0; + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("return 0;",code.print()); + } + + @Test + public void testFuzzerBadLCA() { + new CodeGen(""" + int v0=arg; + if(v0&&--v0?0:arg) {} + """).driver(CodeGen.Phase.LocalSched); + } + + + // Recursive search (TODO: gzip, archives) and delete all .o files + + + @Test + public void testConstantClassInitialization() throws IOException { + TestC.runSF("val x = 42; return 0;","constantClassInt","",0); + TestC.runSF("val f = { -> 7; }; return 0;","constantClassFunction","",0); + } + + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/MutabilityTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/MutabilityTest.java new file mode 100644 index 00000000..8bd77c06 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/MutabilityTest.java @@ -0,0 +1,67 @@ +package com.seaofnodes.simple; + +import org.junit.Test; +import static org.junit.Assert.*; +import com.seaofnodes.simple.codegen.CodeGen; + +public class MutabilityTest { + private static void ok(String src, String result) { + var code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals(result,Eval2.eval(code,0)); + } + private static void bad(String src) { + try { + new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + } catch( RuntimeException expected ) { + String msg = expected.getMessage(); + assertTrue("Expected a permission error, got: "+expected, + msg!=null && (msg.contains("final") || msg.contains("not of declared type") || msg.contains("Cannot store") || msg.contains("Argument #"))); + return; + } + fail("Accepted forbidden write: "+src); + } + + @Test public void testIndependentBindingsAndViews() { + ok("struct P { int x; new P = { int v -> x=v; }; }; !P ~p=new P(0); p.x=7; return p.x;", "7"); + ok("struct P { int x; new P = { int v -> x=v; }; }; ~P !p=new P(3); p=new P(7); return p.x;", "7"); + bad("struct P { int x; new P = { int v -> x=v; }; }; !P ~p=new P(0); p=new P(0); return 0;"); + bad("struct P { int x; new P = { int v -> x=v; }; }; ~P !p=new P(0); p.x=7; return 0;"); + bad("int ~x=3; x=7; return x;"); + ok("int !x=3; x=7; return x;", "7"); + } + + @Test public void testDeepViewAndAliases() { + ok("struct P { int x; new P = { int v -> x=v; }; }; !P ~p=new P(0); ~P ~r=p; p.x=7; return r.x;", "7"); + bad("struct P { int x; new P = { int v -> x=v; }; }; struct Box { !P p; new Box = { !P v -> p=v; }; }; !P p=new P(0); Box b=new Box(p); b.p.x=7; return 0;"); + bad("struct P { int x; new P = { int v -> x=v; }; }; P p=new P(0); !P q=p; q.x=7; return 0;"); + bad("struct P { P? next; int x; new P = { P? v -> next=v; }; }; !P p=new P(new P(null)); p.next.x=7; return 0;"); + bad("struct P { !P? next; int x; new P = { !P? v -> next=v; }; }; P p=new P(new P(null)); p.next.x=7; return 0;"); + } + + @Test public void testInferencePreservesAccess() { + ok("struct P { int x; }; val p=new P; p.x=7; return p.x;", "7"); + bad("struct P { int x; }; val p=new P; p=new P; return 0;"); + bad("struct P { int x; }; P p=new P; var q=p; q.x=7; return 0;"); + } + + @Test public void testParameterPermissions() { + ok("struct P { int x; }; val f={ !P p -> p.x=7; return p.x; }; return f(new P);", "7"); + ok("val f={ int ~x -> return x; }; return f(7);", "7"); + bad("val f={ int ~x -> x=7; return x; }; return f(3);"); + bad("struct P { int x; }; val f={ P p -> p.x=7; return p.x; }; return f(new P);"); + bad("struct P { int x; }; val f={ !P p -> p.x=7; return p.x; }; P p=new P; return f(p);"); + bad("struct P { int x; }; val f={ P?[] a -> return a#; }; !P?[] a=new !P?[1]; return f(a);"); + } + + @Test public void testArrayLayers() { + bad("struct P { int x; }; !P?[] rw=new !P?[1]; P?[] ro=rw; rw[0]=new P; ro[0].x=7; return 0;"); + ok("u8[] a=new u8[2]; a[0]=7; return a[0];", "7"); + ok("struct P { int x; new P = { int v -> x=v; }; }; P?[] a=new P?[1]; a[0]=new P(7); return a[0].x;", "7"); + bad("struct P { int x; new P = { int v -> x=v; }; }; P?[] a=new P?[1]; a[0]=new P(0); a[0].x=7; return 0;"); + ok("struct P { int x; new P = { int v -> x=v; }; }; !P?[] a=new !P?[1]; a[0]=new P(0); a[0].x=7; return a[0].x;", "7"); + bad("u8[~] a=new u8[1]; a[0]=7; return 0;"); + ok("u8[~]?[] rows=new u8[~]?[1]; rows[0]=new u8[1]; return rows#;", "1"); + bad("u8[~]?[] rows=new u8[~]?[1]; rows[0]=new u8[1]; rows[0][0]=7; return 0;"); + bad("u8[]?[~] rows=new u8[]?[1]; rows[0]=new u8[1]; return 0;"); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java new file mode 100644 index 00000000..fa914d28 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java @@ -0,0 +1,68 @@ +package com.seaofnodes.simple; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeTrue; + +public class NativeExecutionTest { + // A subprocess with deterministic stdout, stderr, and process status. + public static class Child { + public static void main(String[] args) { + System.out.println("before exit"); + System.err.println("child diagnostic"); + System.exit(Integer.parseInt(args[0])); + } + } + + private static String child(int exit) throws IOException { + String java = Path.of(System.getProperty("java.home"),"bin","java").toString(); + return TestC.exec(java,"-cp",System.getProperty("java.class.path"), + Child.class.getName(),Integer.toString(exit)); + } + + private static void checkFailure(int exit) throws IOException { + try { + child(exit); + fail("Accepted process exit "+exit); + } catch( IOException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("exit code: "+exit)); + assertTrue(e.getMessage(),e.getMessage().contains("before exit")); + assertTrue(e.getMessage(),e.getMessage().contains("child diagnostic")); + } + } + + @Test public void testNonzeroExit() throws IOException { checkFailure(7); } + + @Test public void testFullWindowsExitCode() throws IOException { + assumeTrue(TestC.OS.startsWith("Windows")); + checkFailure(256); // Narrowing to a byte incorrectly reports success. + checkFailure(2816); // Observed Cygwin status for the Hello World crash. + } + + @Test public void testSuccessfulOutput() throws IOException { + assertEquals("before exit"+System.lineSeparator(),child(0)); + } + + @Test public void testHelloWorldArtifacts() throws IOException { + new Chapter22Test().testHelloWorld(); + // Model a parallel run paused between Chapter 22's link and execution. + // Chapter 25 must not replace the executable Chapter 22 is about to run. + String exe = "build/objs/helloWorld"+(TestC.OS.startsWith("Windows") ? ".exe" : ""); + assertEquals("Hello, World!",TestC.exec(exe)); + } + + @Test public void testDriverNonzeroExit() throws Exception { + Path dir = Files.createTempDirectory(Path.of("build/objs"),"driver-exit-"); + Path src = dir.resolve("fail.smp"); + Files.writeString(src,"return 7;"); + try { + Simple.main(new String[]{src.toString()}); + fail("Driver accepted a failed executable"); + } catch( IOException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("exit code: 7")); + } + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/PhaseTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/PhaseTest.java new file mode 100644 index 00000000..5ff7fcd6 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/PhaseTest.java @@ -0,0 +1,43 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.graph.GraphObserver; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.CodeGen.Phase; +import com.seaofnodes.simple.node.Node; +import java.util.ArrayList; +import org.junit.Test; +import static org.junit.Assert.*; + +public class PhaseTest { + private static final String SRC = "int sum=0; for(int i=0; i<arg; i++) sum+=i; return sum;"; + + @Test public void testIdealPhasesAndResume() { + ArrayList<String> phases = new ArrayList<>(); + CodeGen code = new CodeGen(SRC); + code._obs = new GraphObserver<Node>() { + @Override public void before(Node n) {} + @Override public void after(Node n, Node repl, boolean applied) {} + @Override public void phase(String phase) { phases.add(phase); } + }; + code.driver(Phase.TypeCheck); + code.driver(Phase.TypeCheck); // A repeated request does not rerun anything. + code.driver(Phase.LocalSched); + assertEquals("Parse Iter Opto IterPostOpto FinalizeIR TypeCheck Lower LoopTree Schedule LocalSched", + String.join(" ",phases)); + assertNull(code._mach); + assertEquals("45",Eval2.eval(code,10)); + } + + @Test public void testEncodingAndResume() { + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) { + CodeGen code = new CodeGen(SRC).driver(Phase.TypeCheck); + code.driver(Phase.LoopTree,cpu,"SystemV"); + code.driver(Phase.Select,cpu,"SystemV"); + code.driver(Phase.Encoding,cpu,"SystemV"); + byte[] staged = code._encoding._bits.toByteArray(); + CodeGen direct = new CodeGen(SRC).driver(Phase.Encoding,cpu,"SystemV"); + assertArrayEquals(staged,direct._encoding._bits.toByteArray()); + } + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/Simple.java b/chapter-modules/src/test/java/com/seaofnodes/simple/Simple.java new file mode 100644 index 00000000..437fcd81 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/Simple.java @@ -0,0 +1,254 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.CompUnit; +import com.seaofnodes.simple.codegen.CodeGen.Phase; +import com.seaofnodes.simple.print.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; + +public class Simple { + static void usage() { + System.out.println( +""" +simple [options] input-file.smp ...program args... +Options are applied in-order. +Options: + --help - This text + + --root root/ - Project root directory; this must end in a '/' and input + files are relative paths to this. If missing, the one input + file path is treated as its own root directory. + + -o output - Project output directory; similar to --root, if 'output' ends in a + '/', then all result files use relative paths to 'output', using a input file + root-relative path. If --root is missing, the one result file is named 'output' + + --eval - (slowly) evaluate the compiled code in emulator + --run - run the compiled code natively; this is the default + --main - export the top-level initializer as C `main` + --cpu <cpu-name> - use specific CPU (x86_64_v2, riscv, arm) + --abi <abi-name> - use specific ABI variant (SystemV) + --target - print native CPU and ABI + --version + -S - dump generated assembler code + --dump-size - print the size of generated code + --dump-time - print compilation and execution times + --dump-after-parse - dump intermediate representation after parse + --dump-after-iter - dump intermediate representation after pessimistic iter pass + --dump-after-opto - dump intermediate representation after SCCP + --dump-after-iter-post-opto - dump after SCCP-driven peepholes and inlining + --dump-after-finalize-ir - dump after field layout and IR finalization + --dump-after-type-check - dump intermediate representation after type check pass + --dump-after-lower - dump expanded class initialization before loop trees + --dump-after-select - dump intermediate representation after instruction selection pass + --dump-after-gcm - dump intermediate representation after GCM pass + --dump-after-local-sched - dump intermediate representation after local scheduling pass + --dump-after-reg-alloc - dump intermediate representation after register allocation pass + --dump-after-encode - dump intermediate representation after encoding pass + --dump-after-all - dump intermediate representation after all passes + --dump - dump final intermediate representation only +""" +); + System.exit(0); + } + + static void bad_usage() { throw bad("Invalid usage (use --help)"); } + static RuntimeException bad(String err) { + System.err.println("ERROR: "+err); + System.exit(1); + return new RuntimeException(err); + } + + static void target() { + System.out.print(TestC.CPU_PORT); + System.out.print("-"); + System.out.println(TestC.CALL_CONVENTION); + } + + static void print_compilation_times(CodeGen code) { + long[] times = code._times; + for( int i=0; i<code._times.length; i++ ) + System.out.printf("%20s %.3f sec%n", Phase.values()[i], times[i]); + } + + static String getInput() { + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + StringBuilder text=new StringBuilder(); + try { + for( String line; (line=reader.readLine())!=null; ) text.append(line).append('\n'); + } catch( IOException e ) { throw new RuntimeException(e); } + return text.toString(); + } + + public static void main(String[] args) throws Exception { + String root = null; + String input_filename = null; + boolean do_eval = false; + boolean do_run = true; + boolean do_codegen = false; + boolean emit_main = false; + boolean print_time = false; + boolean print_size = false; + boolean print_asm = false; + int dump = 0; + int first_arg = 0; + String src = null; + String cpu = null; + String abi = null; + String out = null; + + // Parse command line + int i; for( i = 0; i < args.length; i++ ) { + String arg = args[i]; + if( arg.charAt(0) != '-' ) { + input_filename = arg; + break; + } + + switch (arg) { + case "-": input_filename=""; break; + case "--dump-after-parse": dump |= 1<<Phase.Parse .ordinal() ; break; + case "--dump-after-iter": dump |= 1<<Phase.Iter .ordinal() ; break; + case "--dump-after-opto": dump |= 1<<Phase.Opto .ordinal() ; break; + case "--dump-after-iter-post-opto": dump |= 1<<Phase.IterPostOpto.ordinal(); break; + case "--dump-after-finalize-ir": dump |= 1<<Phase.FinalizeIR.ordinal() ; break; + case "--dump-after-type-check": dump |= 1<<Phase.TypeCheck .ordinal() ; break; + case "--dump-after-lower": dump |= 1<<Phase.Lower .ordinal() ; break; + case "--dump-after-loop-tree": dump |= 1<<Phase.LoopTree .ordinal() ; break; + case "--dump-after-select": dump |= 1<<Phase.Select .ordinal() ; break; + case "--dump-after-gcm": dump |= 1<<Phase.Schedule .ordinal() ; break; + case "--dump-after-local-sched": dump |= 1<<Phase.LocalSched.ordinal() ; break; + case "--dump-after-reg-alloc": dump |= 1<<Phase.RegAlloc .ordinal() ; break; + case "--dump-after-encode": dump |= 1<<Phase.Encoding .ordinal() ; break; + case "--dump-after-export": dump |= 1<<Phase.Export .ordinal() ; break; + case "--dump": dump |= 1<<Phase.LastPhase .ordinal() ; break; + case "--dump-after-all": dump |= (2<<Phase.LastPhase .ordinal())-1; break; + case "-S": print_asm = true; break; + case "--eval": do_eval = true ; do_run = false; break; + case "--run": do_run = true ; break; + case "--norun": do_run = false; break; + case "--main": emit_main = true; break; + case "--dump-size": print_size = true; break; + case "--dump-time": print_time = true; break; + case "-o": if (out != null || i + 1 >= args.length || args[i + 1].charAt(0) == '-') bad_usage(); + out = args[++i]; do_codegen=true; + break; + case "--root": if( root!=null || i+1>=args.length ) bad_usage(); + root=args[++i]; + break; + case "--cpu": if (cpu != null || i + 1 >= args.length || args[i + 1].charAt(0) == '-') bad_usage(); + cpu = args[++i]; + break; + case "--abi": if (abi != null || i + 1 >= args.length || args[i + 1].charAt(0) == '-') bad_usage(); + abi = args[++i]; + break; + case "--target": target(); break; + case "--version": throw Utils.TODO(); + case "--help": usage(); + default: bad_usage(); + } + } + + // Break the paths into these parts: + // - A module path, given from "--root" or inferred from the one input file + // - Some sources [modpath/relpath/file.smp]*, all relative to the module path. + // - A build path, given from "-o" or inferred from the one input file + // + // All the sources are compiled, with outputs in [buildpath/relpath/file.o]* + + if (input_filename == null) throw bad("no input file' (use --help)"); + if( !input_filename.isEmpty() && !input_filename.endsWith(".smp") ) + throw bad("File extension must be .smp"); + Path inPath = Path.of(input_filename); + + // Figure out the module path + Path modPath; + if( root == null ) { + modPath = inPath.getParent(); + if( modPath == null ) + modPath = Path.of("."); + } else { + modPath=Path.of(root); + inPath=modPath.resolve(inPath); + } + + // Compute the module-relative paths for all sources + Path srcRelPath = modPath.relativize(inPath); + int n = srcRelPath.getNameCount(); + Path relPath = n==1 ? null : srcRelPath.subpath(0,n-1); + // Compute source/class name + String srcNameExt = srcRelPath.getName(n-1).toString(); + String srcName = srcRelPath.toString().replace('\\','/'); + srcName=srcName.substring(0,srcName.length()-4); + + // Compute the build path. + Path outPath; + if( out == null ) { + // If no output given, the build dir is the modPath + outPath = modPath; + } else { + // If the output is a directory, that is our out path + outPath = Path.of(out); + if( !out.endsWith("/") ) { + // If the output is a file, the directory is our outPath + Path fullOut = outPath; + outPath = outPath.getParent(); + if( outPath == null ) + outPath = Path.of("."); + Path foo = outPath.relativize(fullOut); + if( !foo.toString().equals(srcName+".o") ) + throw bad("Output path needs to be of the form "+srcName+".o"); + } + } + String outBase = outPath.resolve(srcName).toString(); + + if( do_run || print_asm || print_size ) { + if (cpu == null) cpu = TestC.CPU_PORT; + if (abi == null) abi = TestC.CALL_CONVENTION; + do_codegen = true; + } + if( cpu == null ) cpu = TestC.CPU_PORT; + if( abi == null ) abi = TestC.CALL_CONVENTION; + + // Read input file + src = input_filename.isEmpty() ? getInput() : null; + + // Compilation pipeline + CodeGen code = new CodeGen(modPath.toString(), outPath.toString(), srcName, src, 456, true, TypeInteger.BOT ); + code.driver(do_eval && !do_codegen ? Phase.TypeCheck : Phase.LastPhase, + cpu,abi, false, emit_main || do_run || do_eval, dump); + + if( do_codegen && print_asm ) + System.out.println(code.asm()); + + if( print_size ) + System.out.printf( "Code Size: %d%n", code._encoding._bits.size()); + + if( print_time ) + print_compilation_times(code); + + if( do_eval ) { + // TODO: Support for evaluation of functions with different argument numbers and types + long t = System.currentTimeMillis(); + long arg = (i+1 < args.length) ? Integer.parseInt(args[i+1]) : 0; + System.out.println(Eval2.eval(code, arg, 100000)); + if( print_time ) + System.out.printf( "EXECUTION TIME: %.3f sec%n", (System.currentTimeMillis() - t) / 1e3); + } + if( do_run ) { + if ( !TestC.CPU_PORT.equals( cpu ) || !TestC.CALL_CONVENTION.equals( abi ) ) + throw bad("cannot run code on not native target"); + String exe = TestC.OS.startsWith("Windows") ? outBase+".exe" : outBase; + String result = TestC.gcc(outBase+".o", null, null, "", TestC.linkObjs(null), exe); + System.out.print(result); + } + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/SpillStats.java b/chapter-modules/src/test/java/com/seaofnodes/simple/SpillStats.java new file mode 100644 index 00000000..944f7ef5 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/SpillStats.java @@ -0,0 +1,92 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.type.TypeInteger; +import java.nio.file.*; +import java.util.Map; +import java.util.TreeMap; +import org.junit.runner.*; +import org.junit.runner.notification.*; + +// Earlier cohorts freeze source/target membership; Chapter 25 retains native checks. +public class SpillStats extends RunListener { + private static SpillStats ACTIVE; + private Description _test; + private int _qualityFailures; + private final Map<String,long[]> _totals = new TreeMap<>(); + + @Override public void testStarted(Description test) { _test = test; } + @Override public void testFinished(Description test) { _test = null; } + + public static void record25(CodeGen code) { + if( ACTIVE==null || ACTIVE._test==null || ACTIVE._test.getTestClass()!=ModulesTest.class ) return; + ACTIVE.record(code,"Chapter25",ACTIVE._test.getMethodName(),code._mach.getClass().getSimpleName(),code._callingConv); + } + + private void record(CodeGen code, String cohort, String test, String cpu, String abi) { + for( String key : new String[]{cohort+","+cpu+","+abi,cohort+",ALL,ALL"} ) { + long[] sum = _totals.computeIfAbsent(key,k -> new long[3]); + sum[0]++; sum[1] += code._regAlloc._spills; sum[2] += code._regAlloc._spillScaled; + } + System.out.println("allocation,"+cohort+","+test+","+cpu+","+abi+","+ + code._regAlloc._spills+","+code._regAlloc._spillScaled); + } + + // Keep native result checks running during a quality comparison. + public static void checkSpills(int expected, int actual) { + if( expected==-1 || CodeGen.iterSeedOverridden() ) return; + try { org.junit.Assert.assertEquals("Expect spills:",expected,actual,Math.max(1,expected>>3)); } + catch( AssertionError error ) { + if( ACTIVE==null ) throw error; + ACTIVE._qualityFailures++; + System.err.println(ACTIVE._test+": "+error.getMessage()); + } + } + + public static void main(String[] args) throws Exception { + ACTIVE = new SpillStats(); + Path dir = Path.of("src/test/java/com/seaofnodes/simple/spill"); + int failures=0, attempts=0; + for( String line : Files.readAllLines(dir.resolve("cohorts.tsv")) ) { + if( line.startsWith("#") || line.isBlank() ) continue; + attempts++; + String[] row = line.split("\t"); + try { + String src = Files.readString(dir.resolve(row[5])); + CodeGen code = new CheckedCodeGen(null,null,null,src,123L,true,TypeInteger.BOT) + .driver(CodeGen.Phase.RegAlloc,row[2],row[3]); + ACTIVE.record(code,row[0],row[1],row[2],row[3]); + } catch( Throwable error ) { + failures++; + System.err.println(line); + error.printStackTrace(); + } + } + // Include the library's allocation even when make's sys.o is up to date. + try { + CodeGen code = new CheckedCodeGen("src/main/smp","build/spill-sys","sys", + Files.readString(Path.of("src/main/smp/sys.smp")),456L,true,TypeInteger.BOT) + .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION); + ACTIVE.record(code,"Chapter25","sys",TestC.CPU_PORT,TestC.CALL_CONVENTION); + } catch( Throwable error ) { + failures++; + error.printStackTrace(); + } + JUnitCore junit = new JUnitCore(); + junit.addListener(ACTIVE); + Result result = junit.run(ModulesTest.class); + for( Failure failure : result.getFailures() ) System.err.println(failure.getTrace()); + failures += result.getFailureCount(); + if( failures!=0 ) System.out.println("FAILED: totals are not a complete comparison."); + System.out.println("cohort,cpu,abi,allocations,splits,scaled"); + for( var row : ACTIVE._totals.entrySet() ) { + long[] sum = row.getValue(); + System.out.println(row.getKey()+","+sum[0]+","+sum[1]+","+sum[2]); + } + System.out.println("Historical allocations: "+attempts+"; native tests: "+result.getRunCount()+"; failures: "+failures+"; spill golden failures: "+ACTIVE._qualityFailures); + failures += ACTIVE._qualityFailures; + ACTIVE = null; + if( failures!=0 ) System.exit(1); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/StaticDataTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/StaticDataTest.java new file mode 100644 index 00000000..b8f5f1d1 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/StaticDataTest.java @@ -0,0 +1,172 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.StaticData; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.StoreNode; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.isa.eval.EvalArm64; +import com.seaofnodes.isa.eval.EvalRisc5; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; + +public class StaticDataTest { + private static final String SRC="val n=42; val f={int x -> x+n;}; return 0;"; + + private static StaticData.ObjectData root(CodeGen code) { + for( var obj : code._encoding._data.entries() ) + if( obj.symbol.endsWith("Test.$class") ) return obj; + throw new AssertionError("Missing Test class object"); + } + + // Read the image before <clinit>, then call the function through its data slot. + @Test public void testEmulatorImage() { + for( String cpu : new String[]{"arm","riscv"} ) { + CodeGen code=new CodeGen(SRC).driver(CodeGen.Phase.TypeCheck); + assertNull("Constant fields fold before instruction selection", + code._stop.walk(n -> n instanceof StoreNode st && + (st._name.equals("n") || st._name.equals("f")) ? n : null)); + assertNotNull(code._stop.walk(n -> n instanceof ConstantNode && + n._type instanceof TypeMem mem && mem.isConstant() && + mem._t==TypeInteger.constant(42) ? n : null)); + assertNotNull("The runtime initialization flag still needs its Store", + code._stop.walk(n -> n instanceof StoreNode st && + st._name.equals("$init") ? n : null)); + code.driver(cpu,"SystemV",true,false); + var e=code._encoding; + var obj=root(code); + TypeStruct ts=(TypeStruct)obj.type; + int cp=(e._bits.size()+15)&-16, sd=(cp+e._cpool.size()+15)&-16; + byte[] image=new byte[1<<20]; + System.arraycopy(e._bits.buf(),0,image,0,e._bits.size()); + System.arraycopy(e._cpool.buf(),0,image,cp,e._cpool.size()); + System.arraycopy(e._sdata.buf(),0,image,sd,e._sdata.size()); + int n=sd+obj.offset+ts.offset(ts.find("n")); + int f=sd+obj.offset+ts.offset(ts.find("f")); + if( cpu.equals("arm") ) { + var arm=new EvalArm64(image,1<<16); + assertEquals(42,arm.ld4s(n)); + arm._pc=arm.ld4s(f); + arm.regs[0]=7; + assertEquals(0,arm.step(1000)); + assertEquals(49,arm.regs[0]); + } else { + var r5=new EvalRisc5(image,1<<16); + assertEquals(42,r5.ld4s(n)); + r5._pc=r5.ld4s(f); + r5.regs[10]=7; + assertEquals(0,r5.step(1000)); + assertEquals(49,r5.regs[10]); + } + } + } + + @Test public void testNativeImage() throws Exception { + Path dir=Path.of("build/objs/static-data"); + Files.createDirectories(dir); + CodeGen code=new CodeGen(null,dir.toString(),"Test",SRC,123L,true, + TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + TypeStruct ts=(TypeStruct)root(code).type; + Path driver=dir.resolve("driver.c"); + Files.writeString(driver,""" + #include <stdint.h> + #include <string.h> + extern unsigned char data[] __asm__("Test.$class"); + typedef int64_t (__attribute__((CALL_CONV)) *fun)(int64_t); + int main(void) { + int64_t n; uint32_t f; + memcpy(&n,data+%d,sizeof(n)); + memcpy(&f,data+%d,sizeof(f)); + return n!=42 || ((fun)(uintptr_t)f)(7)!=49; + } + """.formatted(ts.offset(ts.find("n")),ts.offset(ts.find("f")))); + String exe=dir.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString(); + TestC.linkExe(dir.resolve("Test.o").toString(), + TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi", + driver.toString(),null,exe); + assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); + } + + // Native entries must agree too, both in static fields (before <clinit>) + // and when code materializes the address. The native body may live above 4G. + @Test public void testNativeFunctionIdentity() throws Exception { + Path dir=Path.of("build/objs/static-native-functions"); + Files.createDirectories(dir); + int[] offsets=new int[2]; + for( int i=0; i<2; i++ ) { + String name=i==0 ? "A" : "B"; + String src="{ i32 -> i32 } abs = \"C\"; val get"+name+"={ -> abs; }; return 0;"; + CodeGen code=new CodeGen(null,dir.toString(),name,src,123L,true,TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); + assertNull("The native address is initialized in the static image", + code._stop.walk(n -> n instanceof StoreNode st && st._name.equals("abs") ? n : null)); + code.driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + for( var obj : code._encoding._data.entries() ) + if( obj.symbol.equals(name+".$class") ) { + TypeStruct ts=(TypeStruct)obj.type; + offsets[i]=ts.offset(ts.find("abs")); + } + } + Path driver=dir.resolve("driver.c"); + Files.writeString(driver,""" + #include <stdint.h> + #include <string.h> + extern unsigned char a[] __asm__("A.$class"), b[] __asm__("B.$class"); + typedef int32_t (__attribute__((CALL_CONV)) *fun)(int32_t); + __attribute__((CALL_CONV)) extern fun getA(void), getB(void); + int main(void) { + uint32_t x,y; + memcpy(&x,a+%d,sizeof(x)); + memcpy(&y,b+%d,sizeof(y)); + fun f=(fun)(uintptr_t)x; + if(!x || x!=y || f!=getA() || f!=getB()) return 1; + return f(-7)!=7; + } + """.formatted(offsets[0],offsets[1])); + String exe=dir.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString(); + String conv=TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi"; + // Either object's weak definition can win; every reference must agree. + for( String first : new String[]{"A","B"} ) { + String second=first.equals("A")?"B":"A"; + TestC.linkExe(dir.resolve(first+".o").toString(),conv,driver.toString(), + new Ary<>(new String[]{dir.resolve(second+".o").toString()}),exe); + assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); + } + } + + // Two source files must reference the same mutable class object. + @Test public void testSharedClass() throws Exception { + Path dir=Path.of("build/objs/static-data-shared"); + Files.createDirectories(dir); + Files.createDirectories(dir.resolve("Main")); + String[] names={"Globals","A","B"}; + String[] sources={"int count=0;", + "val a={ -> Globals; };", "val b={ -> Globals; };"}; + for( int i=0; i<names.length; i++ ) + Files.writeString(dir.resolve("Main/"+names[i]+".smp"),sources[i]); + new CodeGen(dir.toString(),dir.toString(),"Main","val a={ -> A.a(); }; val b={ -> B.b(); };",123L,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + Path driver=dir.resolve("driver.c"); + Files.writeString(driver,""" + #include <stdint.h> + __attribute__((CALL_CONV)) extern int64_t *a(void),*b(void); + int main(void) { + int64_t *x=a(),*y=b(); + if(x!=y) return 1; + *x=37; + return *y!=37; + } + """); + String exe=dir.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString(); + TestC.linkExe(dir.resolve("Main.o").toString(), + TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi", + driver.toString(),null,exe); + assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/TestArm64.java b/chapter-modules/src/test/java/com/seaofnodes/simple/TestArm64.java new file mode 100644 index 00000000..f1089cf0 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/TestArm64.java @@ -0,0 +1,53 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.isa.eval.EvalArm64; + +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.cpus.arm.arm; +import com.seaofnodes.simple.util.BAOS; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +// Runs 32-bit ARM code in an emulator + +public class TestArm64 { + + public static EvalArm64 build( String main, String src, int arg, int spills, boolean print ) throws IOException { + // Compile and export Simple + CodeGen code = new CheckedCodeGen(src).driver("arm", "SystemV",true,false); + + // Allocation quality not degraded + int delta = spills>>3; + if( delta == 0 ) delta =1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) assertEquals("Expect spills:", spills, code._regAlloc._spillScaled, delta); + + // Image + byte[] image = new byte[1<<20]; // A megabyte (1024*1024 bytes) + EvalArm64 ARM = new EvalArm64(image, 1<<16); + int start = 0; // Code at offset 0 + int cpool = fill(code._encoding._bits ,image,start); + int sdata = fill(code._encoding._cpool,image,cpool); + int end = fill(code._encoding._sdata,image,sdata); + + // Initial incoming int arg + ARM.regs[arm.X0] = arg; + // malloc memory starts at 64K and goes up + + // Look up starting PC or zero if not given + if( main!=null ) + for( FunNode fun : code._linker ) + if( fun != null && !fun.isDead() && fun._compunit == code.compunit() && main.equals(fun._name) ) + ARM._pc = code._encoding.opStart(fun); + + return ARM; + } + + // Fill bits into image; return end point rounded up. + private static int fill( BAOS bits, byte[] image, int off) { + System.arraycopy(bits.buf(), 0, image, off, bits.size()); + return (off+bits.size() + 15) & -16; + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/TestC.java b/chapter-modules/src/test/java/com/seaofnodes/simple/TestC.java new file mode 100644 index 00000000..164168f6 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/TestC.java @@ -0,0 +1,273 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.Utils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import static org.junit.Assert.*; + +// Call a C driver program, which calls a Simple-generated .o, and self-checks. +// Expects GCC from the default CLI, compiles C, compiles Simple, links and runs. +public abstract class TestC { + public static final int TEST_TIMEOUT_SECONDS = 60; + + public static final String OS = System.getProperty("os.name"); + public static final String CPU = System.getProperty("os.arch"); + + public static final String CALL_CONVENTION = OS.startsWith("Windows") ? "win64" : "SystemV"; + public static final String CPU_PORT = switch( CPU ) { + case "amd64" -> "x86_64_v2"; + default -> throw Utils.TODO("Map Yer CPU Port Here"); + }; + public static final String CPU_ABI = CPU_PORT+"_"+CALL_CONVENTION; + public static final String RELEASE_SYS_DIR = "build/release/"+CPU_ABI; + + public static String runtimeObject() { + String home = System.getenv("SIMPLE_HOME"); + String root = home==null || home.isEmpty() ? "build/release" : home; + return Path.of(root,CPU_ABI,"simple_crt.o").toString(); + } + + public static final String C_DRIVERS_DIR = "src/test/java/com/seaofnodes/simple/progs/"; + + /** + * Compile, link and run + * - WITH a C driver + * - using the default OS/CPU calling convention. + * - no lib sys + */ + public static void runC( String src, String base, String expected, int spills) throws IOException { + String cfile = C_DRIVERS_DIR+base+".c"; + run(src, base, null, CALL_CONVENTION, "", cfile, expected, spills); + } + + /** + * Compile, link and run + * - withOUT a C driver + * - using the default OS/CPU calling convention. + * - no lib sys + */ + public static void runSF( String src, String base, String expected, int spills ) throws IOException { + run(src, base, null, CALL_CONVENTION, null,null, expected,spills); + } + + /** + * Compile, link and run + * - withOUT a C driver + * - using the default OS/CPU calling convention. + * - WITH a test lib sys + */ + public static void runSYS( String src, String base, String expected, int spills ) throws IOException { + Ary<String> externPaths = new Ary<>(new String[]{RELEASE_SYS_DIR}); + run(src, base, externPaths, CALL_CONVENTION, null,null, expected,spills); + } + + + /** + * Compile, link and run - general case + * + * @param src Program source to compile + * @param base Compiled binary final base name + * @param externPaths External symbol search path + * @param simple_conv Argument convention when calling Simple code + * @param c_conv Argument convention when calling C code, or null if not + * linking against a C driver program + * @param cfile Associated C program which will drive the Simple program, + * or null if not linking against a C driver program + * @param expected Expected stdout string + * @param spills Expected limit on spill code, used to validate the + * register allocation is reasonable + */ + public static void run( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv, String cfile, String expected, int spills ) throws IOException { + run(src, base, externPaths, simple_conv, c_conv, cfile, null, expected, spills); + } + + public static void runArgs( String src, String base, Ary<String> externPaths, String simple_conv, + String expected, int spills, String... programArgs ) throws IOException { + run0(src,base,externPaths,simple_conv,null,null,null,expected,spills,programArgs); + } + + public static void run( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv, String cfile, String stdin, String expected, int spills ) throws IOException { + run0(src,base,externPaths,simple_conv,c_conv,cfile,stdin,expected,spills,new String[0]); + } + + private static void run0( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv, + String cfile, String stdin, String expected, int spills, String[] programArgs ) throws IOException { + String exe = compile(src,base,externPaths,simple_conv,c_conv,cfile,spills); + + String[] execArgs = new String[programArgs.length+1]; + execArgs[0] = exe; + System.arraycopy(programArgs,0,execArgs,1,programArgs.length); + String result = execStdin(stdin, 0, execArgs ); + assertEquals(expected,result); + } + + public static String compile( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv, + String cfile, int spills ) throws IOException { + // Simple file base-name example: + // foo.smp -> + // build/objs/foo.o - object file + // build/objs/foo.exe - linked executable Windows + // build/objs/foo - linked executable Linux + String pathBase = "build/objs/"+base; + String obj = pathBase+".o"; + String exe = pathBase+(OS.startsWith("Windows") ? ".exe" : ""); + // Compile simple, emit ELF + CodeGen code = new CheckedCodeGen(null,"build/objs",base,src,126L,true,TypeInteger.BOT); + code.driver( CPU_PORT, simple_conv, false, cfile==null ); + + linkExe(obj, c_conv, cfile, linkObjs(externPaths), exe); + + // Allocation quality not degraded + SpillStats.checkSpills(spills,code._regAlloc._spillScaled); + return exe; + } + + // Link with gcc, and execute the resulting binary, returning stdout as a + // String. Any errors *assert* instead of returning some error code; this + // utility is meant to execute inside a JUnit test which will catch the + // assert and flag the test as failed. + public static String gcc( String main, double ignore, String... objs ) throws IOException { + String BLDDIR = "build/objs/test0/"; + String exe = BLDDIR+main+(OS.startsWith("Windows") ? ".exe" : ""); + linkExe(objs[0],null,null,null,exe); + // Module tests intentionally use the program exit code as their result. + ExecResult result = execute(null,0,exe); + return result.exit==0 ? result.out : "exec exit code: "+result.exit; + } + + // Link with gcc, and execute the resulting binary, returning stdout as a + // String. Any errors *assert* instead of returning some error code; this + // utility is meant to execute inside a JUnit test which will catch the + // assert and flag the test as failed. + public static String gcc( String obj, String c_conv, String cfile, boolean stdin, String... args ) throws IOException { + return gcc(obj,c_conv,cfile,stdin ? "" : null,null,args); + } + + public static String gcc( String obj, String c_conv, String cfile, String stdin, String... args ) throws IOException { + return gcc(obj,c_conv,cfile,stdin,null,args); + } + + public static String gcc( String obj, String c_conv, String cfile, String stdin, Ary<String> linkObjs, String... args ) throws IOException { + linkExe(obj,c_conv,cfile,linkObjs,args[0]); + return execStdin(stdin,0,args); + } + + public static void linkExe( String obj, String c_conv, String cfile, Ary<String> linkObjs, String exe ) throws IOException { + // Compile the C program. Compiling code and constants in the low + // 2Gig. Pointers are 64b BUT since always in the low 2G all the + // high bits are zero - and Simple code can be emitted treating + // pointers as 4bytes. + var params = new Ary<>(String.class); + params.add("gcc"); + params.add(OS.startsWith("Windows") ? "-Wl,--image-base,0x400000" : "-no-pie"); + if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main` + params.add(obj); + // Standalone Simple programs export `simple_main`; the small C + // runtime owns the native main symbol and converts argc/argv into a + // normal Simple array before making the ABI-qualified call. + if( cfile==null ) params.add(runtimeObject()); + if( linkObjs != null ) + params.addAll(linkObjs.asAry()); + params.addAll(new String[] { + "-lm", // Picks up 'sqrt' for newtonFloat tests to compare + "-g", + "-o", + exe, + }); + // Calling convention for C calls, if any + if( cfile!=null ) { + params.add("-D"); + params.add("CALL_CONV="+c_conv); + } + + // Run GCC to link (optionally compile C driver code) + exec(TEST_TIMEOUT_SECONDS,params.asAry()); + } + + public static String exec( String... args ) throws IOException { + return execStdin(null,0,args); + } + + // Tests can bound execution without imposing a deadline on the CLI driver. + public static String exec( int seconds, String... args ) throws IOException { + return execStdin(null,seconds,args); + } + + private static String execStdin( String stdin, int seconds, String... args ) throws IOException { + ExecResult result = execute(stdin,seconds,args); + if( result.exit!=0 ) + throw new IOException(String.join(" ",args)+"\nexec exit code: "+result.exit+ + "\nstdout:\n"+result.out+"\nstderr:\n"+result.err); + return result.out; + } + + private record ExecResult(int exit, String out, String err) {} + + private static ExecResult execute( String stdin, int seconds, String... args ) throws IOException { + // Execute results + ProcessBuilder smp = new ProcessBuilder(args); + Path stdinFile = null; + Path stdoutFile = Files.createTempFile("simple-stdout",".txt"); + Path stderrFile = Files.createTempFile("simple-stderr",".txt"); + Process p = null; + try { + smp.redirectOutput(stdoutFile.toFile()); + smp.redirectError(stderrFile.toFile()); + if( stdin!=null && stdin.isEmpty() ) smp.redirectInput(ProcessBuilder.Redirect.INHERIT); + else if( stdin!=null ) { + stdinFile = Files.createTempFile("simple-stdin",".txt"); + Files.writeString(stdinFile,stdin); + smp.redirectInput(stdinFile.toFile()); + } + p = smp.start(); + if( stdin==null ) + p.getOutputStream().close(); + if( seconds>0 && !p.waitFor(seconds,TimeUnit.SECONDS) ) { + String process = "pid="+p.pid()+", cpu="+ + p.info().totalCpuDuration().map(Object::toString).orElse("unknown"); + p.destroyForcibly(); + boolean stopped = p.waitFor(5,TimeUnit.SECONDS); + throw new IOException(String.join(" ",args)+"\ntimed out after "+seconds+" seconds ("+process+")"+ + (stopped ? "" : "; process did not terminate")+ + "\nstdout:\n"+Files.readString(stdoutFile)+ + "\nstderr:\n"+Files.readString(stderrFile)); + } + // Windows statuses have 32 bits; narrowing can turn a failure into 0. + int exit = seconds>0 ? p.exitValue() : p.waitFor(); + return new ExecResult(exit,Files.readString(stdoutFile),Files.readString(stderrFile)); + } catch( InterruptedException e ) { + if( p!=null ) p.destroyForcibly(); + Thread.currentThread().interrupt(); + throw new IOException("interrupted",e); + } finally { + if( stdinFile!=null ) Files.deleteIfExists(stdinFile); + Files.deleteIfExists(stdoutFile); + Files.deleteIfExists(stderrFile); + } + } + + static Ary<String> linkObjs(Ary<String> externPaths) { + if( externPaths == null ) + return null; + Ary<String> objs = new Ary<>(String.class); + for( String path : externPaths ) { + File file = new File(path); + if( file.isFile() && file.getName().endsWith(".o") ) + objs.add(file.toString()); + else if( file.isDirectory() ) { + File sys = new File(file,"sys.o"); + if( sys.isFile() ) + objs.add(sys.toString()); + } + } + return objs.isEmpty() ? null : objs; + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/TestRisc5.java b/chapter-modules/src/test/java/com/seaofnodes/simple/TestRisc5.java new file mode 100644 index 00000000..10c75d3f --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/TestRisc5.java @@ -0,0 +1,55 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.isa.eval.EvalRisc5; + +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.cpus.riscv.riscv; +import com.seaofnodes.simple.util.BAOS; +import java.io.IOException; + +import static org.junit.Assert.*; + +// Runs 32-bit R5 code in an emulator +public abstract class TestRisc5 { + + public static EvalRisc5 build( String src, String main, int arg, int spills, boolean print ) throws IOException { + // Compile and export Simple + CodeGen code = new CheckedCodeGen(src).driver("riscv", "SystemV",true,false); + + // Allocation quality not degraded + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); + + // Image + byte[] image = new byte[1<<20]; // A megabyte (1024*1024 bytes) + EvalRisc5 R5 = new EvalRisc5(image, 1<<16); + int start = 0; // Code at offset 0 + int cpool = fill(code._encoding._bits ,image,start); + int sdata = fill(code._encoding._cpool,image,cpool); + int end = fill(code._encoding._sdata,image,sdata); + + // Initial incoming int arg + R5.regs[riscv.A0] = arg; + // malloc memory starts at 64K and goes up + + // Look up starting PC or zero if not given + if( main!=null ) + for( FunNode fun : code._linker ) + if( fun != null && !fun.isDead() && fun._compunit == code.compunit() && main.equals(fun._name) ) + R5._pc = code._encoding.opStart(fun); + + return R5; + } + + // Fill bits into image; return end point rounded up. + private static int fill( BAOS bits, byte[] image, int off) { + System.arraycopy(bits.buf(), 0, image, off, bits.size()); + return (off+bits.size() + 15) & -16; + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/TestRunner.java b/chapter-modules/src/test/java/com/seaofnodes/simple/TestRunner.java new file mode 100644 index 00000000..5389967e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/TestRunner.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple; + +import org.junit.internal.TextListener; +import org.junit.runner.JUnitCore; +import org.junit.runner.Request; + +/** Make's single-class/single-method JUnit entry point. */ +public class TestRunner { + public static void main(String[] args) throws ClassNotFoundException { + String name=args[0]; + Class<?> test=Class.forName(name.contains(".") ? name : "com.seaofnodes.simple."+name); + Request request=args[1].isEmpty() ? Request.aClass(test) : Request.method(test,args[1]); + JUnitCore runner=new JUnitCore(); + runner.addListener(new TextListener(System.out)); + System.exit(runner.run(request).wasSuccessful() ? 0 : 1); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/TypeTest.java b/chapter-modules/src/test/java/com/seaofnodes/simple/TypeTest.java new file mode 100644 index 00000000..f6099607 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/TypeTest.java @@ -0,0 +1,223 @@ +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 { + + // Removing zero must not reset the widening history of a remaining range. + @Test public void testNonZeroWidening() { + for( byte widen=0; widen<=3; widen++ ) { + TypeInteger pos = TypeInteger.make(0,255,widen).nonZero(); + TypeInteger neg = TypeInteger.make(-255,0,widen).nonZero(); + Assert.assertEquals("positive range widening",widen,pos._widen); + Assert.assertEquals("negative range widening",widen,neg._widen); + assertSame(TypeInteger.make(1,255,widen),pos); + assertSame(TypeInteger.make(-255,-1,widen),neg); + // Singleton ranges retain their widening level too. + assertSame(TypeInteger.make(1,1,widen),TypeInteger.make(0,1,widen).nonZero()); + assertSame(TypeInteger.make(-1,-1,widen),TypeInteger.make(-1,0,widen).nonZero()); + } + Assert.assertNull(TypeInteger.ZERO.nonZero()); + } + + // Struct hashes omit field types, so interning compares unlike leaf kinds. + @Test + public void testCyclicLeafKinds() { + TypeStruct bottom = TypeStruct.make("B14LeafKinds",false, + Field.make("x",Type.BOTTOM,2,false)); + TypeStruct one = TypeStruct.make("B14LeafKinds",false, + Field.make("x",TypeFloat.constant(1.0),2,false)); + TypeStruct two = TypeStruct.make("B14LeafKinds",false, + Field.make("x",TypeFloat.constant(2.0),2,false)); + + // Meeting creates a new struct while cyclic equality is active. Its + // float leaf must compare unequal to the previously interned BOTTOM. + TypeStruct meet = (TypeStruct)one.meet(two); + assertSame(TypeFloat.F32,meet._fields[0]._t); + assertSame(meet,two.meet(one)); + assertSame(meet,TypeStruct.make("B14LeafKinds",false, + Field.make("x",TypeFloat.F32,2,false))); + assertSame(bottom,meet.meet(bottom)); + assertSame(meet.dual(),one.dual().join(two.dual())); + } + + // Test basic type properties + @Test + public void testTypeAdHoc() { + Assert.assertEquals( TypeScalar.BOT, TypeInteger.TRUE.meet(TypeNil.NIL) ); + Assert.assertEquals( TypeScalar.BOT, TypeInteger.TOP .meet(TypeNil.NIL) ); + + TypeStruct s1 = TypeStruct.make("s1",false, + Field.make("a", TypeInteger.BOT,-1, false), + Field.make("b", TypeInteger.BOT,-2, false) ); + TypeStruct s2 = TypeStruct.make("s2",false, + Field.make("a", TypeInteger.BOT,-3, false), + Field.make("b", TypeInteger.BOT,-4, false) ); + TypeMemPtr p1 = TypeMemPtr.make(s1); + Assert.assertNotEquals(s1, s1.dual()); + + TypeMem m1 = TypeMem.make(2,TypeNil.NIL); + TypeMem m2 = TypeMem.make(3,TypeInteger.U16); + TypeMem m3 = TypeMem.make(4,TypeFloat.F64); + TypeMem m4 = TypeMem.make(5,TypeInteger.BOT); + + Assert.assertNotEquals(m1, m2); + Assert.assertNotEquals(m2, m3); + Assert.assertNotEquals(m3, m4); + + Assert.assertEquals(TypeStruct.BOT, s1.meet(s2)); + TypeMem scalarMemBot = TypeMem.make(1,TypeScalar.BOT,false,false,false,XInt.FULL,XInt.FULL); + Assert.assertEquals(scalarMemBot, ((TypeMem)m1.meet(m2)).makeFrom( XInt.FULL, XInt.FULL)); + Assert.assertEquals(TypeMem.make(1,TypeScalar.BOT), m1.meet(m3)); + Assert.assertEquals(scalarMemBot, ((TypeMem)m3.meet(m4)).makeFrom( XInt.FULL, XInt.FULL)); + + Assert.assertEquals(TypeMem.make(2,Type.XNIL,true,true,true, XInt.FULL, XInt.FULL), m1.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.assertEquals(ptr1, ptr1.dual().dual()); + Assert.assertEquals(TypeMemPtr.makeNullable(TypeStruct.BOT).withAccess(false), ptr1.meet(ptr2nil)); + + 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.withAccess(false), 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(); + + // Cyclic check + TypeStruct S1 = ((TypeMemPtr)TypeStruct.SFLT1.field("s1")._t)._obj.close(); + Assert.assertFalse(s1.isFinal()); + Type s1ro = S1.makeRO(); + Type s1ro2 = S1.makeRO(); + Assert.assertSame(s1ro,s1ro2); + Assert.assertTrue(s1ro.isFinal()); + + Assert.assertFalse(S1.isConstant()); + } + + // 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(t0+" isa "+t1+" joined "+t2, 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(t0+" & "+t1+" mt="+mt, ta,d1); + assertSame(t0+" & "+t1+" mt="+mt, 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(t0+" & "+t1+" & "+t2, t01_2,t0_12); + } + + // Test cyclic types and meets + @Test + public void testCyclic0() { + Type d0 = TypeStruct.SFLT1.dual(); + Type d1 = d0.dual(); + assertSame(TypeStruct.SFLT1,d1); + } + + @Test + public void testList() { + TypeStruct list = TypeStruct.open("List"); + TypeMemPtr plist = TypeMemPtr.makeNullable(list); + list = list.add(Field.make("next", plist, 2, false) ); + list = list.add(Field.make("x", TypeInteger.BOT, 3, false)); + // Make a cyclic type + list = list.close(); + // Fields are mutable + Assert.assertFalse(list.isFinal()); + TypeStruct flist = (TypeStruct)list.makeRO(); + Assert.assertTrue(flist.isFinal()); + Assert.assertNotSame( list, flist ); + } + + @Test + public void testOpen() { + TypeStruct open = TypeStruct.make("List",false); + TypeMemPtr plist = TypeMemPtr.makeNullable(open); + TypeStruct list = open.add(Field.make("next", plist, 2, false) ); + list = list.add(Field.make("x", TypeInteger.BOT, 3, false)); + // Make a cyclic type + list = list.close(); + Type mt = open.meet(list); + Assert.assertEquals(list,mt); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java b/chapter-modules/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java new file mode 100644 index 00000000..445acb61 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java @@ -0,0 +1,141 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.isa.eval.EvalArm64; +import com.seaofnodes.simple.TestC; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.cpus.arm.arm; +import com.seaofnodes.simple.node.cpus.x86_64_v2.x86_64_v2; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import static org.junit.Assert.*; + +// Force overlapping flags, then force every GPR unavailable across the gap. +public class FlagSpillTestSupport { + private static class Op extends MachConcreteNode { + final RegMask _def, _use, _kill; + Op(RegMask def,RegMask use,RegMask kill,Node... ins) { + super(ins); _def=def; _use=use; _kill=kill; _type=TypeInteger.BOT; + } + @Override public String op() { return "flagsProbe"; } + @Override public RegMask outregmap() { return _def; } + @Override public RegMask regmap(int i) { return _use; } + @Override public RegMask killmap() { return _kill; } + @Override public void encoding(Encoding enc) { throw new AssertionError(); } + } + + public static void run() throws IOException { + StringBuilder asm = new StringBuilder(".text\n"); + for( boolean isArm : new boolean[]{false,true} ) + for( boolean stack : new boolean[]{false,true} ) { + CodeGen code = new CodeGen(""); + code._callingConv="SystemV"; + code._mach=isArm ? new arm(code) : new x86_64_v2(code); + RegionNode block = new RegionNode(null,code._start,code._start); + code._cfg.add(block); + int flagsReg=isArm ? 64 : 32, firstStack=flagsReg+1; + RegMask flags = new RegMask(flagsReg); + Op def = new Op(flags,null,null,block); + RegMask killMask = stack ? (isArm ? new RegMask(0x7fffffffL,1L) : new RegMask(0x1ffffffefL)) : flags; + Op kill = new Op(null,null,killMask,block); + new Op(null,flags,null,block,def); + RegAlloc alloc = new RegAlloc(code); + alloc.regAlloc(); + + int splits=0, slots=0; + for( Node n : block._outputs ) if( n instanceof SplitNode split ) { + int dst=alloc.regnum(n), src=alloc.regnum(n.in(1)); + assertTrue(split.outregmap().test(dst)); + assertTrue(split.regmap(1).test(src)); + for( int fp=isArm ? 32 : 16; fp<flagsReg; fp++ ) { + assertFalse("FP output allowed",split.outregmap().test(fp)); + assertFalse("FP input allowed",split.regmap(1).test(fp)); + } + if( dst>=firstStack || src>=firstStack ) slots++; + splits++; + } + assertTrue(splits>=2); + assertEquals(stack,slots!=0); + + // Keep the allocated registers, supplying a frame for encoding stack offsets. + FunNode frame = new FunNode(null,null,"flags",null,(Node)null); + code._regAlloc=new RegAlloc(code); + for( Node n : block._outputs ) + if( alloc.regnum(n)>=0 ) code._regAlloc.newLRG(n,frame)._reg=alloc.regnum(n); + Encoding save = new Encoding(code), restore = new Encoding(code); + Encoding enc = save; + for( Node n : block._outputs ) { + if( n==kill ) enc=restore; + if( n instanceof SplitNode split ) split.encoding(enc); + } + if( isArm ) { + for( int bits=0; bits<16; bits++ ) { + EvalArm64 cpu = new EvalArm64(new byte[1024],512); + System.arraycopy(save._bits.buf(),0,cpu._buf,0,save._bits.size()); + System.arraycopy(restore._bits.buf(),0,cpu._buf,save._bits.size(),restore._bits.size()); + cpu.N=(bits&8)!=0; cpu.Z=(bits&4)!=0; cpu.C=(bits&2)!=0; cpu.V=(bits&1)!=0; + assertEquals(0,cpu.step(save._bits.size()/4)); + cpu.N=cpu.Z=cpu.C=cpu.V=false; + if( stack ) for( int r=0; r<31; r++ ) cpu.regs[r]=0; + assertEquals(0,cpu.step(restore._bits.size()/4)); + assertEquals((bits&8)!=0,cpu.N); assertEquals((bits&4)!=0,cpu.Z); + assertEquals((bits&2)!=0,cpu.C); assertEquals((bits&1)!=0,cpu.V); + assertEquals(512,cpu.regs[31]); + } + } else { + Encoding body = new Encoding(code); + int[] preserved={3,5,6,7,12,13,14,15}; + for( int r : preserved ) pushPop(body,r,false); + X86.imm(body,0x81,5,4,16); // Reserve spill storage. + X86.flags(body,TestC.OS.startsWith("Windows") ? 1 : 7,true); + bytes(body,save); + if( stack ) { + for( int r=0; r<16; r++ ) if( r!=4 ) X86.constant(body,r,0); + } else X86.reg(body,0x39,0,0); // CMP rax,rax clobbers only flags. + bytes(body,restore); + X86.flags(body,0,false); // Return the restored flags. + X86.imm(body,0x81,0,4,16); + for( int i=preserved.length-1; i>=0; i-- ) pushPop(body,preserved[i],true); + X86.ret(body); + String name=stack ? "flags_stack" : "flags_reg"; + asm.append(".globl ").append(name).append('\n').append(name).append(":\n.byte "); + for( int i=0; i<body._bits.size(); i++ ) { + if( i!=0 ) asm.append(','); + asm.append(body._bits.buf()[i]&255); + } + asm.append('\n'); + } + } + Path dir=Files.createTempDirectory(Path.of("build/objs"),"flag_spills_"); + Files.writeString(dir.resolve("flags.s"),asm); + Files.writeString(dir.resolve("flags.c"),""" + typedef unsigned long long u64; + u64 __attribute__((CALL_CONV)) flags_reg(u64); + u64 __attribute__((CALL_CONV)) flags_stack(u64); + int main(void) { + const int positions[]={0,2,4,6,7,11}; + for(int bits=0; bits<64; bits++) { + u64 flags=0x202; + for(int i=0;i<6;i++) if(bits&(1<<i)) flags|=1ULL<<positions[i]; + if((flags_reg(flags)&0x8d5)!=(flags&0x8d5)) return 1; + if((flags_stack(flags)&0x8d5)!=(flags&0x8d5)) return 2; + } + return 0; + } + """); + String exe=dir.resolve(TestC.OS.startsWith("Windows") ? "flags.exe" : "flags").toString(); + TestC.exec("gcc","-DCALL_CONV="+(TestC.OS.startsWith("Windows") ? "ms_abi" : "sysv_abi"), + "-o",exe,dir.resolve("flags.c").toString(),dir.resolve("flags.s").toString()); + TestC.exec(exe); + } + + private static void bytes(Encoding dst,Encoding src) { + for( int i=0; i<src._bits.size(); i++ ) dst.add1(src._bits.buf()[i]&255); + } + private static void pushPop(Encoding enc,int reg,boolean pop) { + if( reg>=8 ) enc.add1(0x41); + enc.add1((pop ? 0x58 : 0x50)+(reg&7)); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java b/chapter-modules/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java new file mode 100644 index 00000000..b255bcf7 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java @@ -0,0 +1,76 @@ +package com.seaofnodes.simple.codegen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import static org.junit.Assert.*; +public class PrintRegTestSupport { + public static void check() throws Exception { + var code = new CodeGen("return 0;").parse(); + var alloc = new RegAlloc(code); + var n = new ConstantNode(TypeInteger.ZERO); + var a = new LRG((short)1,null); + var b = new LRG((short)2,null); + var c = new LRG((short)3,null); + a._reg = -1; + c._leader = b; + b._leader = a; + var field = RegAlloc.class.getDeclaredField("_lrgs"); + field.setAccessible(true); + var map = (java.util.Map<Node,LRG>)field.get(alloc); + map.put(n,c); + assertEquals("V1",alloc.reg(n)); + assertEquals(-1,alloc.regnum(n)); + assertSame(c,map.get(n)); + assertSame(b,c._leader); + assertSame(a,b._leader); + } + public static void checkConstantPool() throws Exception { + var code = new CodeGen("return 0;").parse(); + code._phase = CodeGen.Phase.Encoding; + var enc = new Encoding(code); + var t = com.seaofnodes.simple.type.TypeStruct.make("PrintPool",false, + com.seaofnodes.simple.type.Field.make("x",TypeInteger.ZERO,2,true)); + var n = new ConstantNode(TypeInteger.ZERO); + enc.largeConstant(n,t,0,2); + int size = t.size(); // Encoding lays out data before printing the pool. + for( int i=0; i<size; i++ ) enc._cpool.write(0); + // Layout queries are forbidden during printing, even if they would + // happen to return a cached answer without changing the type. + boolean[] printing = {false}; + var scalar = new com.seaofnodes.simple.type.Type(com.seaofnodes.simple.type.Type.BOTTOM._type) { + @Override public int alignment() { + assertFalse("alignment queried by printer",printing[0]); + return 0; + } + @Override public int size() { + assertFalse("size queried by printer",printing[0]); + return 1; + } + @Override public String str() { return "LayoutProbe"; } + }; + var scalarNode = new ConstantNode(TypeInteger.TRUE); + enc._bigCons.put(scalarNode,new Encoding.Relo(scalarNode,scalar,(byte)0,(byte)2)); + enc._cpool.write(0); + printing[0] = true; + var offsets = com.seaofnodes.simple.type.TypeStruct.class.getDeclaredField("_offs"); + offsets.setAccessible(true); + offsets.set(t,null); // Printer must not lazily rebuild the cached layout. + var field = com.seaofnodes.simple.type.Type.class.getDeclaredField("VISIT"); + field.setAccessible(true); + var visit = (java.util.Map<Object,Object>)field.get(null); + var marker = new Object(); + visit.put(marker,t); + var method = com.seaofnodes.simple.print.ASMPrinter.class.getDeclaredMethod( + "printConstantPool",int.class,com.seaofnodes.simple.util.SB.class, + com.seaofnodes.simple.util.BAOS.class,java.util.HashMap.class,boolean.class,String.class); + method.setAccessible(true); + try { + var sb = new com.seaofnodes.simple.util.SB(); + method.invoke(null,0,sb,enc._cpool,enc._bigCons,true,"Constant Pool"); + assertTrue(sb.toString().contains("PrintPool")); + assertTrue(sb.toString().contains("LayoutProbe")); + assertNull(offsets.get(t)); + assertSame(t,visit.get(marker)); + assertEquals(1,visit.size()); + } finally { visit.clear(); } + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java b/chapter-modules/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java new file mode 100644 index 00000000..4398827e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java @@ -0,0 +1,352 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.cpus.x86_64_v2.x86_64_v2; +import com.seaofnodes.simple.type.TypeInteger; +import static org.junit.Assert.*; + +// Small scheduled machine graphs isolate register constraints from peepholes. +public class RegAllocTestSupport { + private static final RegMask A = new RegMask(0), B = new RegMask(1); + private static class Op extends MachConcreteNode { + final RegMask _def, _use, _kill; + final boolean _clone; + Op(RegMask def, RegMask use, RegMask kill, boolean clone, Node... ins) { + super(ins); _def=def; _use=use; _kill=kill; _clone=clone; _type=TypeInteger.BOT; + } + @Override public String op() { return "testOp"; } + @Override public RegMask outregmap() { return _def; } + @Override public RegMask regmap(int i) { return _use; } + @Override public RegMask killmap() { return _kill; } + @Override public boolean isClone() { return _clone; } + @Override public Node copy() { return new Op(_def,_use,_kill,_clone,(Node)null); } + @Override public void encoding(Encoding enc) { throw new AssertionError(); } + } + private static CodeGen graph() { + CodeGen code = new CodeGen(""); + code._mach = new x86_64_v2(code); + code._cfg.add(code._start); + return code; + } + + private static SplitNode testSplit(CodeGen code, MachNode def) { + LRG lrg = new LRG((short)0,null); + lrg._machDef = def; + return code._mach.split(lrg,"test",(byte)0); + } + + public static void uncoloredFixedNeighbor() throws Exception { + CodeGen code = graph(); + RegAlloc alloc = new RegAlloc(code); + Op def = new Op(new RegMask(3L),null,null,false,code._start); + LRG lrg = alloc.newLRG(def,null); + LRG neighbor = new LRG((short)99,null); + neighbor._mask = A; + lrg.addNeighbor(neighbor); // Uncolored, but its only possible color is A. + var bias = IFG.class.getDeclaredMethod("biasColorNeighbors",RegAlloc.class,Node.class,RegMask.class); + bias.setAccessible(true); + assertEquals((short)1,bias.invoke(null,alloc,def,new RegMaskRW(3L,0L))); + } + + public static void masks() { + for( int reg : new int[]{0,63,64,65,127} ) { + RegMask mask = new RegMask(reg); + assertEquals(reg,mask.firstReg()); + assertEquals(1,mask.size()); + } + assertEquals(-1,new RegMask(0L).firstReg()); + assertFalse(new RegMask(0L).size1()); + RegMask boundary = new RegMask(Long.MIN_VALUE,Long.MIN_VALUE|1); + assertEquals(64,boundary.nextReg((short)63)); + assertEquals(127,boundary.nextReg((short)64)); + assertEquals(-1,boundary.nextReg((short)127)); + } + + public static void union() { + CodeGen code = graph(); + Op use = new Op(null,B,null,false,code._start); + LRG a = new LRG((short)1,null), b = new LRG((short)2,null); + a._mask = new RegMask(3L); + b._mask = new RegMask(6L); + b.machUse(use,(short)2,true); + LRG both = a.union(b); + assertNotNull(both._mask); + assertEquals(1,both._mask.firstReg()); + assertEquals(1,both._mask.size()); + assertSame(use,both._machUse); + assertEquals(2,both._uidx); + } + + public static void killWithoutResult() { + CodeGen code = graph(); + Op def = new Op(A,null,null,false,code._start); + new Op(null,null,A,false,code._start); // Clobbers A, produces no value. + new Op(null,A,null,false,code._start,def); + RegAlloc alloc = new RegAlloc(code); + assertTrue(BuildLRG.run(0,alloc)); + assertFalse("A live value cannot survive a register kill",IFG.build(0,alloc)); + } + + public static void nullUseMask() { + CodeGen code = graph(); + Op def = new Op(A,null,null,false,code._start); + new Op(null,null,null,false,code._start,def); // Scheduling dependency only. + RegAlloc alloc = new RegAlloc(code); + assertTrue(BuildLRG.run(0,alloc)); + assertTrue(IFG.build(0,alloc)); + } + + public static void cloneRegisterClass() { + for( RegMask required : new RegMask[]{B,new RegMask(6L)} ) { + CodeGen code = graph(); + Op def = new Op(A,null,null,true,code._start); + Op use = new Op(null,required,null,false,code._start,def); + Op use2 = new Op(null,required,null,false,code._start,def); + RegAlloc alloc = new RegAlloc(code); + alloc.regAlloc(); // Re-cloning an A-only value cannot satisfy B/C uses. + assertEquals(0,alloc.regnum(def)); + assertTrue(required.test(alloc.regnum(use.in(1)))); + assertTrue(required.test(alloc.regnum(use2.in(1)))); + assertTrue(use.in(1) instanceof SplitNode); + } + } + + public static void commutativePhi() { + CodeGen code = graph(); + Op a = new Op(A,null,null,false,code._start); + Op b = new Op(A,null,null,false,code._start); + Op add = new Op(A,A,null,false,code._start,a,b) { + @Override public boolean commutes() { return true; } + }; + RegionNode region = new RegionNode(null,code._start,code._start); + new PhiNode("sum",region,add,a); + code._cfg.add(region); + assertTrue(BuildLRG.run(0,new RegAlloc(code))); + } + + private static class CallOp extends CallNode implements MachNode { + CallOp(Node ctrl, Node def) { super(null,ctrl,null,def,def); } + @Override public RegMask regmap(int i) { return i==2 ? A : i==3 ? B : null; } + @Override public RegMask outregmap() { return null; } + @Override public void encoding(Encoding enc) { throw new AssertionError(); } + } + + public static void popularUses() { + for( int calls=0; calls<=2; calls++ ) { + CodeGen code = graph(); + RegAlloc alloc = new RegAlloc(code); + RegionNode block = new RegionNode(null,code._start,code._start); + Op def = new Op(new RegMask(7L),null,null,false,block); + LRG lrg = alloc.newLRG(def,null); + lrg._machDef = def; + lrg._mask = new RegMask(0L); + Op ab = new Op(null,new RegMask(3L),null,false,block,def); + Op bc = new Op(null,new RegMask(6L),null,false,block,def); + Op a = new Op(null,A,null,false,block,def,def); + Op b = new Op(null,B,null,false,block,def); + Op dep = new Op(null,null,null,false,block,def); + CallOp call = calls>0 ? new CallOp(block,def) : null; + if( calls==2 ) new CallOp(block,def); + boolean split = alloc.splitEmptyMaskByUse((byte)0,lrg); + assertEquals(calls<2,split); + if( !split ) { assertSame(def,a.in(1)); assertSame(def,b.in(1)); continue; } + // Two shared copies, even with overlapping masks and repeated inputs. + assertTrue(a.in(1) instanceof SplitNode); + assertTrue(b.in(1) instanceof SplitNode); + assertNotSame(a.in(1),b.in(1)); + assertSame(a.in(1),a.in(2)); + assertSame(b.in(1),ab.in(1)); + assertSame(b.in(1),bc.in(1)); + assertSame(def,dep.in(1)); // A scheduling dependency has no register. + assertEquals(3,def.nOuts()); // Two copies and the dependency. + if( call!=null ) { + assertSame(a.in(1),call.in(2)); + assertSame(b.in(1),call.in(3)); + } + } + } + + // Check the scheduled graph before encoding adds untyped branches or rewrites tail calls. + public static class CheckedCodeGen extends CodeGen { + public CheckedCodeGen(String src) { super(src); } + public CheckedCodeGen(String modDir, String buildDir, + String srcName, String src, long seed, boolean reset, TypeInteger arg) { + super(modDir,buildDir,srcName,src,seed,reset,arg); + } + @Override public CodeGen regAlloc() { + super.regAlloc(); + checkRegisters(this); + com.seaofnodes.simple.SpillStats.record25(this); + return this; + } + } + + public static void topPhi() { + // Either arm can be the synthetic exit. Both predecessors share a + // block here so making TOP live would conflict with the real value. + for( int slot=1; slot<=2; slot++ ) { + CodeGen code = graph(); + RegAlloc alloc = new RegAlloc(code); + Op def = new Op(A,null,null,false,code._start); + Node top = new ConstantNode(com.seaofnodes.simple.type.Type.TOP); + RegionNode region = new RegionNode(null,code._start,code._start); + PhiNode phi = new PhiNode("value",region,slot==1 ? top : def,slot==2 ? top : def); + phi._type = TypeInteger.BOT; + new Op(null,A,null,false,region,phi); + code._cfg.add(region); + assertTrue(BuildLRG.run(0,alloc)); + assertNull(alloc.lrg(top)); + assertSame(alloc.lrg(def),alloc.lrg(phi)); + assertTrue(IFG.build(0,alloc)); + alloc.insertBefore(phi,slot,"test",(byte)0,alloc.lrg(phi)); + assertSame(top,phi.in(slot)); + } + } + + public static void checkRegisters(CodeGen code) { + assertTrue(code._regAlloc.verifyFunctionLocalEdges()); + for( CFGNode bb : code._cfg ) + for( Node n : bb._outputs ) { + if( n instanceof MachNode mach ) { + if( code._regAlloc.regnum(n)>=0 && mach.outregmap()!=null ) + assertTrue(n.getClass().getSimpleName()+"#"+n._nid,code._regAlloc.regnum(n)>=0 && mach.outregmap().test(code._regAlloc.regnum(n))); + for( int i=n instanceof ParmNode ? 2 : 1; i<n.nIns(); i++ ) + if( n.in(i)!=null && code._regAlloc.regnum(n.in(i))>=0 && mach.regmap(i)!=null ) + assertTrue(n.getClass().getSimpleName()+"#"+n._nid+" input "+i,code._regAlloc.regnum(n.in(i))>=0 && mach.regmap(i).test(code._regAlloc.regnum(n.in(i)))); + if( mach.twoAddress()!=0 ) + assertEquals(code._regAlloc.regnum(n),code._regAlloc.regnum(n.in(mach.twoAddress()))); + } + if( n instanceof PhiNode && code._regAlloc.regnum(n)>=0 ) + for( int i=n instanceof ParmNode ? 2 : 1; i<n.nIns(); i++ ) + if( n.in(i)._type==com.seaofnodes.simple.type.Type.TOP ) + assertNull(code._regAlloc.lrg(n.in(i))); + else + assertEquals(code._regAlloc.regnum(n),code._regAlloc.regnum(n.in(i))); + } + } + + public static void copyClobber() throws Exception { + for( boolean killed : new boolean[]{false,true} ) { + CodeGen code = graph(); + RegAlloc alloc = new RegAlloc(code); + Op def = new Op(A,null,null,false,code._start); + alloc.newLRG(def,null)._reg=0; + SplitNode hi = testSplit(code,def); + hi.setDef(0,code._start); hi.setDef(1,def); + alloc.newLRG(hi,null)._reg=1; + new Op(null,null,killed ? A : null,false,code._start); + SplitNode lo = testSplit(code,hi); + lo.setDef(0,code._start); lo.setDef(1,hi); + alloc.newLRG(lo,null)._reg=0; + Op use = new Op(null,A,null,false,code._start,lo); + var post = RegAlloc.class.getDeclaredMethod("postColor"); + post.setAccessible(true); post.invoke(alloc); + assertSame(killed ? lo : def,use.in(1)); + } + CodeGen pre = graph(); + RegAlloc uncolored = new RegAlloc(pre); + Op def0 = new Op(A,null,null,false,pre._start); + uncolored.newLRG(def0,null)._mask=A; + Op fixed = new Op(A,null,null,false,pre._start); + uncolored.newLRG(fixed,null)._mask=A; + SplitNode copy = testSplit(pre,def0); + copy.setDef(0,pre._start); copy.setDef(1,def0); + assertFalse("A fixed definition clobbers even before coloring",uncolored.sameBlockNoClobber(copy)); + } + + public static void coldLoopSelfConflict() { + for( boolean phiConflict : new boolean[]{false,true} ) { + CodeGen code = graph(); + RegAlloc alloc = new RegAlloc(code); + Op init = new Op(A,null,null,false,code._start); + LoopNode loop = new LoopNode(null,null,code._start); + PhiNode phi = new PhiNode("loop",loop,init,null); + phi._type = TypeInteger.BOT; + Op back = new Op(A,A,null,false,loop,phi); + phi.setDef(2,back); + Op use = new Op(null,A,null,false,loop,phi); + loop.setDef(2,loop); + LRG lrg = alloc.newLRG(phi,null); + lrg._mask = A; + alloc.union(lrg,init); alloc.union(lrg,back); + lrg.selfConflict(back); + if( phiConflict ) lrg.selfConflict(phi); + assertTrue(alloc.splitSelfConflict((byte)0,lrg)); + assertSame(phi,use.in(1)); // First try leaves the hot definition alone. + assertSame(back,phi.in(2)); + if( phiConflict ) assertTrue(phi.in(1) instanceof SplitNode); + // Even if the first attempt made no graph change, retry must split. + assertTrue(alloc.splitSelfConflict((byte)1,lrg)); + assertTrue(phi.in(2) instanceof SplitNode); + if( phiConflict ) assertTrue(use.in(1) instanceof SplitNode); + } + } + + public static void copyForward() throws Exception { + // Forwarding, fixed operand, two-address tie, intervening clobber, + // another user, and a scheduling-only edge. + for( int kind=0; kind<6; kind++ ) { + CodeGen code = graph(); + RegAlloc alloc = new RegAlloc(code); + Op def = new Op(A,null,null,false,code._start); + alloc.newLRG(def,null)._reg=0; + SplitNode copy = testSplit(code,def); + copy.setDef(0,code._start); copy.setDef(1,def); + alloc.newLRG(copy,null)._reg=1; + if( kind==3 ) new Op(null,null,A,false,code._start); + int twoAddress = kind==2 ? 1 : 0; + RegMask mask = kind==1 ? B : kind==5 ? null : new RegMask(3L); + Op use = new Op(null,mask,null,false,code._start,copy) { + @Override public int twoAddress() { return twoAddress; } + }; + if( kind==4 ) new Op(null,B,null,false,code._start,copy); + var post = RegAlloc.class.getDeclaredMethod("postColor"); + post.setAccessible(true); post.invoke(alloc); + assertSame("copy case "+kind,kind==0 ? def : copy,use.in(1)); + assertEquals(kind==0 ? 0 : 1,alloc._spills); + } + } + + public static void coalescing() { + // Merge, incompatible masks, interference, capacity, and adjacency remapping. + for( int kind=0; kind<5; kind++ ) { + CodeGen code = graph(); + RegAlloc alloc = new RegAlloc(code); + Op def = new Op(new RegMask(3L),null,null,false,code._start); + SplitNode copy = testSplit(code,def); + copy.setDef(0,code._start); copy.setDef(1,def); + Op use = new Op(null,new RegMask(3L),null,false,code._start,copy); + LRG from = alloc.newLRG(def,null), to = alloc.newLRG(copy,null); + from._mask = kind==1 ? A : new RegMask(3L); + to._mask = kind==1 ? B : new RegMask(3L); + from.machDef(def,false); from.machUse(copy,(short)1,false); + to.machDef(copy,false); to.machUse(use,(short)1,false); + LRG left = new LRG((short)100,null), right = new LRG((short)101,null); + if( kind==2 ) { from.addNeighbor(to); to.addNeighbor(from); } + if( kind>=3 ) { + from.addNeighbor(left); left.addNeighbor(from); + if( kind==3 ) { to.addNeighbor(right); right.addNeighbor(to); } + else { to.addNeighbor(left); left.addNeighbor(to); } + } + int fcnt=from.nadj(), tcnt=to.nadj(); + assertTrue(Coalesce.coalesce(0,alloc)); + if( kind==0 || kind==4 ) { + assertSame(def,use.in(1)); + assertSame(alloc.lrg(def),alloc.lrg(copy)); + assertSame(use,alloc.lrg(def)._machUse); + assertEquals(1,alloc.lrg(def)._uidx); + if( kind==4 ) { + assertEquals(1,left.nadj()); + assertSame(alloc.lrg(def),left._adj.at(0)); + assertSame(left,alloc.lrg(def)._adj.at(0)); + } + } else { + assertSame(copy,use.in(1)); + assertNotSame(alloc.lrg(def),alloc.lrg(copy)); + assertEquals(fcnt,from.nadj()); + assertEquals(tcnt,to.nadj()); + } + } + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java b/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java new file mode 100644 index 00000000..55c2c040 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java @@ -0,0 +1,192 @@ +package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.Eval2; +import com.seaofnodes.simple.codegen.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 graphs optimized with different worklist seeds are 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 = 10000; + + /** + * 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 two different optimizer worklist seeds. + * 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, true); + } catch (RuntimeException e1) { + try { + FuzzerUtils.parse(script, 456, true); + } 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, false); + 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.iter().opto().iterPostOpto().finalizeIR(); + 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); + } + + /** + * Run one seed as a regression test. Unlike the exploratory fuzzer path, + * this deliberately does not reduce on failure; old bad seeds should fail + * fast with the seed visible in the JUnit assertion. + */ + public void fuzzPeepsRegression(long seed) { + var rand = new Random(seed); + var sb = new StringBuilder(); + var valid = new ScriptGenerator(rand, sb, true).genProgram(); + try { + runCheck(sb.toString(), valid); + } catch( Throwable e ) { + AssertionError ae = new AssertionError("Fuzzer regression seed failed: "+seed); + ae.initCause(e); + throw ae; + } + } + + + /** + * Check that no exceptions happened. + * @return true if no exception happened + */ + public boolean noExceptions() { + return exceptions.isEmpty(); + } +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java new file mode 100644 index 00000000..9fe2e405 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java @@ -0,0 +1,111 @@ +package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.codegen.CodeGen; +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 and optimize using the given worklist seed + */ + public static CodeGen parse(String script, long workListSeed, boolean resetTypes ) { + var err = System.err; + var out = System.out; + try { + System.setErr(NULL_PRINT_STREAM); + System.setOut(NULL_PRINT_STREAM); + var code = new CodeGen(script, workListSeed, resetTypes); + return code.driver(CodeGen.Phase.LocalSched); + } finally { + System.setErr(err); + System.setOut(out); + } + } + +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java b/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java new file mode 100644 index 00000000..f6e12e11 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java @@ -0,0 +1,177 @@ +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("\\?([^;]+):"), "+"), + 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); + }); + } + +}
\ No newline at end of file diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java b/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java new file mode 100644 index 00000000..c5e94047 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java @@ -0,0 +1,933 @@ +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_NAME_LENGTH = 10; + private static final int MAX_BLOCK_DEPTH = 4; + private static final int MAX_EXPRESSION_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; + + 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 TypeInt TYPE_INT = new TypeInt("int"); + private static final TypeInt TYPE_BOOL = new TypeInt("bool"); + private static final TypeInt TYPE_U8 = new TypeInt("u8"); + private static final List<TypeInt> INTTYPES = new ArrayList<>(){{add(TYPE_INT); add(TYPE_BOOL); add(TYPE_U8); }}; + + 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; + /** + * 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(declType(type)).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, 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, 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, false); + sb.append(";\n"); + addVariable(name, TYPE_INT); + printIndentation().append("while(").append(name).append("<"); + genShift(TYPE_INT, true, false, MAX_EXPRESSION_DEPTH); + sb.append(") {\n"); + indentation += INDENTATION; + depth--; + loopDepth++; + printIndentation().append(name).append("=").append(name).append("+"); + genExpression(TYPE_INT, true, false); + sb.append(";\n"); + genStatements(); + loopDepth--; + depth++; + indentation -= INDENTATION; + printIndentation().append("}\n"); + indentation -= INDENTATION; + printIndentation().append("}"); + popVariables(currScopeStart); + currScopeStart = oldCSS; + return 0; + } + + // This generator mutates references and fields; request both permissions. + private static String declType(Type type) { + return (type instanceof TypeStruct || type instanceof TypeNullable ? "!" : "") + type.name; + } + + /** Generate a declaration statement. */ + public int genDecl() { + var type = getType(); + var name = getVarName(); + // Always make them mutable + sb.append(generateInvalid() ? getRandomName() : declType(type)).append(" !").append(name); + if (!(type instanceof TypeNullable) || random.nextBoolean()) { + sb.append("="); + genExpression(type, true, type==TYPE_BOOL); + } + 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, declared==TYPE_BOOL); + sb.append(";"); + return 0; + } + + /** + * Generate a return statement. + * @return FLAG_STOP + */ + public int genReturn() { + var type = getType(); + sb.append("return "); + genExpression(type, true, false); + sb.append(";"); + return FLAG_STOP; + } + + public void genExpression(Type type, boolean change, boolean preferLogical) { + genExpression(type, change, preferLogical, MAX_EXPRESSION_DEPTH); + } + + public void genExpression(Type type, boolean change, boolean preferLogical, int d) { + if (change && generateInvalid()) type = getType(); + if (d > 0 && random.nextInt(10)==0) { + genLogical(TYPE_INT, true, true, d); + sb.append("?"); + genExpression(type, true, preferLogical, d-1); + sb.append(":"); + genExpression(type, false, preferLogical, d-1); + } else { + genLogical(type, false, preferLogical, d); + } + } + + public void genLogical(Type type, boolean change, boolean preferLogical, int d) { + if (change && generateInvalid()) type = getType(); + genBitwise(type, false, preferLogical, d); + while (d-->0 && random.nextInt(preferLogical?5:50) == 0) { + sb.append(random.nextBoolean()?"&&":"||"); + genBitwise(type, true, preferLogical, d); + } + } + + public void genBitwise(Type type, boolean change, boolean preferLogical, int d) { + if (change && generateInvalid()) type = getType(); + genComparison(type, false, preferLogical, d); + while (type instanceof TypeInt && d-->0 && random.nextInt(preferLogical?50:5)==0) { + sb.append("&|^".charAt(random.nextInt(3))); + genComparison(type, true, preferLogical, d); + } + } + + public void genComparison(Type type, boolean change, boolean preferLogical, int d) { + if (change && generateInvalid()) type = getType(); + if (d > 0 && type instanceof TypeInt && random.nextInt(preferLogical?5:50)==0) { + type = getType(); + genComparison(type, true, preferLogical, d-1); + sb.append(random.nextBoolean()?"==":"!="); + } + genChaining(type, false, preferLogical, d); + } + + public void genChaining(Type type, boolean change, boolean preferLogical, int d) { + if (change && generateInvalid()) type = getType(); + if (d>0 && type instanceof TypeInt && random.nextInt(preferLogical?5:50)==0) { + genShift(TYPE_INT, true, false, d); + char c = random.nextBoolean()?'>':'<'; + sb.append(c); + if (random.nextInt(4)==0) sb.append('='); + genShift(TYPE_INT, true, false, --d); + while (d-->0 && random.nextInt(preferLogical?5:50)==0) { + if (generateInvalid()) c = c=='>' ? '<' : '>'; + sb.append(c); + if (random.nextInt(4)==0) sb.append('='); + genShift(TYPE_INT, true, false, d); + } + } else { + genShift(type, false, preferLogical, d); + } + } + + private static final String[] SHIFTS = {">>", ">>>", "<<"}; + + public void genShift(Type type, boolean change, boolean preferLogical, int d) { + if (change && generateInvalid()) type = getType(); + genAddition(type, false, preferLogical, d); + while (d-- > 0 && type instanceof TypeInt && random.nextInt(50)==0) { + sb.append(SHIFTS[random.nextInt(3)]); + genAddition(type, true, preferLogical, d); + } + } + + public void genAddition(Type type, boolean change, boolean preferLogical, int d) { + if (change && generateInvalid()) type = getType(); + genMultiplication(type, false, preferLogical, d); + while (d-- > 0 && (type instanceof TypeInt || type==TYPE_FLT) && random.nextInt(50)==0) { + sb.append(random.nextBoolean()?" + ":" - "); + genMultiplication(type, true, preferLogical, d); + } + } + + public void genMultiplication(Type type, boolean change, boolean preferLogical, int d) { + if (change && generateInvalid()) type = getType(); + genUnary(type, false, preferLogical, d); + while (d-- > 0 && (type instanceof TypeInt || type==TYPE_FLT) && random.nextInt(50)==0) { + sb.append(random.nextBoolean()?'*':'/'); + genUnary(type, true, preferLogical, d); + } + } + + /** + * Generate a unary expression. + */ + public void genUnary(Type type, boolean change, boolean preferLogical, int d) { + if (change && generateInvalid()) type = getType(); + if (!(type instanceof TypeInt) && type != TYPE_FLT) { + genSuffix(type, false, preferLogical, d); + return; + } + while (d>0 && random.nextInt(5)==0) { + d--; + boolean what = type instanceof TypeInt && random.nextInt(preferLogical?2:20) == 0; + sb.append(what?'!':'-'); + } + if (random.nextInt(10)==0) { + var t = type; + var v = findVisibleVariablesMatching(va->va.type.isa(t)); + if (v != null) { + sb.append(random.nextBoolean()?"--":"++").append(v.name); + return; + } + } + genSuffix(type, false, preferLogical, d); + } + + 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(); + } + + public void genSuffix(Type type, boolean change, boolean preferLogical, int d) { + if (change && generateInvalid()) type = getType(); + if (d > 0 && random.nextBoolean() && genField(type, d-1)) return; + if (d > 0 && random.nextInt(10) == 0) { + if (random.nextBoolean() && genField(type, d-1)) return; + var t = type; + var v = findVisibleVariablesMatching(va -> va.type.isa(t)); + if (v != null) { + sb.append(v.name).append(random.nextBoolean() ? "--" : "++"); + return; + } + } + genPrimary(type, false, preferLogical, d); + } + + public boolean genField(Type type, int d) { + if (generateInvalid()) { + var field = getRandomName(); + var t = getType(); + genSuffix(t, true, false, d); + sb.append(".").append(field); + } else { + var field = getField(type); + if (field == null) return false; + genSuffix(field.struct, true, false, d); + sb.append(".").append(field.name); + } + return true; + } + + /** + * Generate a primary expression. + */ + public void genPrimary(Type type, boolean change, boolean preferLogical, int d) { + if (change && generateInvalid()) type = getType(); + var rand = random.nextInt(10); + if (d > 0 && rand == 0) { + sb.append("("); + genExpression(type, false, preferLogical, d-1); + 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/chapter-modules/src/test/java/com/seaofnodes/simple/progs/arg_count.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/arg_count.c new file mode 100644 index 00000000..e247e28a --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/arg_count.c @@ -0,0 +1,10 @@ +#include <stdio.h> + +__attribute__ ((CALL_CONV)) +extern double addAll( long long i0, double f1, long long i2, double f3, long long i4, double f5, long long i6, double f7, long long i8, double f9, long long i10, double f11, long long i12, double f13, long long i14, double f15, long long i16, double f17, long long i18, double f19 ); + +int main() { + double result = addAll( 0, 1.1, 2, 3.1, 4, 5.1, 6, 7.1, 8, 9.1, 10, 11.1, 12, 13.1, 14, 15.1, 16, 17.1, 18, 19.1 ); + printf("%f\n", result); + return 0; +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/brain_fuck.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/brain_fuck.c new file mode 100644 index 00000000..127cbe73 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/brain_fuck.c @@ -0,0 +1,18 @@ +#include <stdio.h> +#include <stdbool.h> + +typedef struct AryInt { + unsigned int len; + unsigned char data[]; +} AryInt; + +__attribute__ ((CALL_CONV)) +extern AryInt* brain_fuck(); + +int main() { + AryInt* ps = brain_fuck(); + + printf("%.*s", ps->len, ps->data); + + return 0; +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/c_returns.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/c_returns.c new file mode 100644 index 00000000..6d0ff2fe --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/c_returns.c @@ -0,0 +1,28 @@ +#include <stdint.h> +#include <stdio.h> + +uint64_t c_bits; +// Legal narrow C returns: unused RAX bits deliberately contain garbage. +#define RET(name) __asm__(".text\n.globl " #name "\n" #name ":\nmovq c_bits(%rip), %rax\nret\n"); +RET(c_i8) RET(c_u8) RET(c_i16) RET(c_u16) RET(c_i32) RET(c_u32) RET(c_i64) +#define DECL(name) extern int64_t __attribute__((CALL_CONV)) name(void); +DECL(test_i8) DECL(test_u8) DECL(test_i16) DECL(test_u16) +DECL(test_i32) DECL(test_u32) DECL(test_i64) DECL(test_cmp) + +int main(void) { + uint64_t values[] = {0xa5a5a5a500000000ULL, 0xa5a5a5a50000007fULL, + 0xa5a5a5a500000080ULL, 0xa5a5a5a500007fffULL, 0xa5a5a5a500008000ULL, + 0xa5a5a5a57fffffffULL, 0xa5a5a5a580000000ULL, 0xa5a5a5a5ffffffffULL}; + for(unsigned i=0; i<sizeof(values)/sizeof(values[0]); i++) { + c_bits=values[i]; + int64_t actual[]={test_i8(),test_u8(),test_i16(),test_u16(),test_i32(),test_u32(),test_i64(),test_cmp()}; + int64_t expected[]={(int8_t)c_bits,(uint8_t)c_bits,(int16_t)c_bits,(uint16_t)c_bits, + (int32_t)c_bits,(uint32_t)c_bits,(int64_t)c_bits,(int32_t)c_bits==-1}; + for(unsigned j=0; j<8; j++) + if(actual[j]!=expected[j]) { + printf("case %u type %u: %lld != %lld\n",i,j,(long long)actual[j],(long long)expected[j]); + return 1; + } + } + return 0; +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/extern_data.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/extern_data.c new file mode 100644 index 00000000..9325f65e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/extern_data.c @@ -0,0 +1,22 @@ +#include <stdint.h> +#include <stdio.h> + +int32_t counter=7; +int8_t small=-3; +double fraction=1.25; + +int64_t bump(void) { counter+=5; return counter; } +__attribute__((CALL_CONV)) extern int64_t test_global(void); + +int main(void) { + int64_t result=test_global(); + if(result!=71515 || counter!=15 || small!=-2 || fraction!=1.75) { + printf("result=%lld counter=%d small=%d fraction=%g\n", + (long long)result,counter,small,fraction); + return 1; + } + // A second call must observe C's new values, not a Simple initialization copy. + counter=-2; + result=test_global(); + return result!=-19394 || counter!=6 || small!=-1 || fraction!=2.25; +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/extern_data_import.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/extern_data_import.c new file mode 100644 index 00000000..f65e5c84 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/extern_data_import.c @@ -0,0 +1,14 @@ +#include <stdint.h> +#include <stdio.h> + +int32_t counter=7; +__attribute__((CALL_CONV)) extern int64_t test_global(void); + +int main(void) { + int64_t result=test_global(); + if(result!=710 || counter!=10) { + printf("result=%lld counter=%d\n",(long long)result,counter); + return 1; + } + return 0; +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/fib.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/fib.c new file mode 100644 index 00000000..263d2f13 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/fib.c @@ -0,0 +1,16 @@ +#include <stdio.h> +#include <stdbool.h> + + +__attribute__ ((CALL_CONV)) +extern int fib(int x); + +int main() { + printf("["); + for (int i = 0; i < 10; i++) { + printf("%d", fib(i)); + if (i < 9) printf(", "); + } + printf("]"); + return 0; +}
\ No newline at end of file diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/floatToInteger.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/floatToInteger.c new file mode 100644 index 00000000..cf9390d8 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/floatToInteger.c @@ -0,0 +1,25 @@ +#include <stdint.h> +#include <math.h> +#include <stdio.h> + +extern int64_t float_to_int(double); +extern int64_t float_to_u8(double); + +int main(void) { + double values[] = {0.0,-0.0,6.92,-6.92,0.99,-0.99,255.75,-257.75, + 0x1p63,0x1.fffffffffffffp62,-0x1p63,-0x1.0000000000001p63, + INFINITY,-INFINITY,NAN,-NAN}; + for( unsigned i=0; i<sizeof(values)/sizeof(values[0]); i++ ) { + double x=values[i]; + int64_t expected=isnan(x) ? 0 : x>=0x1p63 ? INT64_MAX + : x<=-0x1p63 ? INT64_MIN : (int64_t)x; + int64_t actual=float_to_int(x), narrow=float_to_u8(x); + if( actual!=expected || narrow!=(expected&255) ) { + fprintf(stderr,"conversion of %g: got %lld / %lld, expected %lld / %lld\n", + x,(long long)actual,(long long)narrow, + (long long)expected,(long long)(expected&255)); + return 1; + } + } + return 0; +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/loop2.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/loop2.c new file mode 100644 index 00000000..934f2d89 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/loop2.c @@ -0,0 +1,7 @@ + +__attribute__ ((CALL_CONV)) +extern int loop2(); + +int main() { + return loop2(); +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/newtonFloat.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/newtonFloat.c new file mode 100644 index 00000000..7e7e796d --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/newtonFloat.c @@ -0,0 +1,16 @@ +#include <stdio.h> +#include <math.h> +#include <stdlib.h> + +extern double test_sqrt(double); + +int main( int argc, char** argv ) { + for( int i=0; i<10; i++ ) { + double d = test_sqrt((double)i), expect = sqrt((double)i); + double delta = fabs(d-expect); + printf("%d %f (%g)\n",i,d,delta); + if( delta > 1e-15 ) + return 1; + } + return 0; +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/person.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/person.c new file mode 100644 index 00000000..df95e1ca --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/person.c @@ -0,0 +1,34 @@ +#include <stdio.h> +#include <stdbool.h> +#include <stdlib.h> + +typedef struct Person { + int age; +} Person; + +typedef struct PersonArray { + unsigned int length; + int pad; + Person* data[]; +} PersonArray; + +__attribute__ ((CALL_CONV)) +extern void fcn(PersonArray* people, int index); + +int main() { + Person base[5] = {{25},{1},{18},{45},{60}}; + unsigned char buf[8+8*5]; + PersonArray* people_array = (PersonArray*)buf; + people_array->length=5; + for (int i=0; i<people_array->length; i++) people_array->data[i] = &base[i]; + + fcn(people_array, 1); + fcn(people_array, 1); + fcn(people_array, 1); + fcn(people_array, 1); + fcn(people_array, 1); + + printf("%d\n", people_array->data[1]->age); // expected: 6 + + return 0; +}
\ No newline at end of file diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/sieve.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/sieve.c new file mode 100644 index 00000000..cac4cfa3 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/sieve.c @@ -0,0 +1,19 @@ +#include <stdio.h> +#include <stdbool.h> + +typedef struct AryInt { + unsigned int len; + unsigned int data[]; +} AryInt; + +__attribute__ ((CALL_CONV)) +extern AryInt *sieve(int N); + +int main() { + AryInt* ps = sieve(100); + printf("%d[",ps->len); + for( int i=0; i<ps->len; i++ ) + printf("%d, ",ps->data[i]); + printf("]"); + return 0; +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/sort.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/sort.c new file mode 100644 index 00000000..ad76cc7f --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/sort.c @@ -0,0 +1,28 @@ +#include <stdio.h> +#include <stdlib.h> + +// Test wrapper to call `sort.smp` + +typedef struct AryInt { + unsigned int len; + long long data[]; +} AryInt; + +__attribute__ ((CALL_CONV)) +extern AryInt* merge_sort(AryInt*); + + +long long primes[] = { 25, 97, 89, 83, 79, 73, 71, 67, 61, 59, 53, 47, 43, 41, 37, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2 }; + +int main() { + AryInt *as = (AryInt*)primes; + + merge_sort(as); + + printf("%d[",as->len); + for( int i=0; i<as->len; i++ ) + printf("%d, ",as->data[i]); + printf("]"); + + return 0; +}
\ No newline at end of file diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_10x.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_10x.c new file mode 100644 index 00000000..d44d5387 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_10x.c @@ -0,0 +1,13 @@ +#include <stdio.h> +#include <stdbool.h> +#include <stdlib.h> + +__attribute__ ((CALL_CONV)) +extern int stack10(int x); + +int main() { +int result = stack10(0); + +printf("%d", result); +return 0; +}
\ No newline at end of file diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_11x.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_11x.c new file mode 100644 index 00000000..d71979d1 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_11x.c @@ -0,0 +1,13 @@ +#include <stdio.h> +#include <stdbool.h> +#include <stdlib.h> + +__attribute__ ((CALL_CONV)) +extern int stack11(int x); + +int main() { +int result = stack11(1); + +printf("%d", result); +return 0; +}
\ No newline at end of file diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_12x.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_12x.c new file mode 100644 index 00000000..a4e425cb --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_12x.c @@ -0,0 +1,13 @@ +#include <stdio.h> +#include <stdbool.h> +#include <stdlib.h> + +__attribute__ ((CALL_CONV)) +extern int stack12(int x); + +int main() { +int result = stack12(0); + +printf("%d", result); +return 0; +}
\ No newline at end of file diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_13x.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_13x.c new file mode 100644 index 00000000..a3a51d0e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_13x.c @@ -0,0 +1,13 @@ +#include <stdio.h> +#include <stdbool.h> +#include <stdlib.h> + +__attribute__ ((CALL_CONV)) +extern int stack13(int x); + +int main() { +int result = stack13(0); + +printf("%d", result); +return 0; +}
\ No newline at end of file diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_9x.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_9x.c new file mode 100644 index 00000000..4f93ba24 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stacked_r_9x.c @@ -0,0 +1,13 @@ +#include <stdio.h> +#include <stdbool.h> +#include <stdlib.h> + +__attribute__ ((CALL_CONV)) +extern int stack9(int x); + +int main() { +int result = stack9(0); + +printf("%d", result); +return 0; +}
\ No newline at end of file diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stringHash.c b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stringHash.c new file mode 100644 index 00000000..2c6d0fa9 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/progs/stringHash.c @@ -0,0 +1,30 @@ +#include <stdio.h> +#include <stdbool.h> + +typedef struct ArrayU8 { + unsigned int length; + unsigned char data[]; +} ArrayU8; +typedef struct String { + ArrayU8 * cs; + long long _hashCode; +} String; + + +__attribute__ ((CALL_CONV)) +extern bool equals(String*st1, String*st2); + +int main() { + String test1 = {.cs=(ArrayU8*)"\x04\x00\x00\x00test"}; + String test2 = {.cs=(ArrayU8*)"\x05\x00\x00\x00test1"}; + + if( equals(&test1, &test2) ) { + printf("Expected test1 and test2 unequal"); + return 1; + } + if( !equals(&test2, &test2) ) { + printf("Expected test2 and test2 equal"); + return 1; + } + return 0; +} diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/BrainFuckTest-testBrainFuck.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/BrainFuckTest-testBrainFuck.smp new file mode 100644 index 00000000..71af2b7a --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/BrainFuckTest-testBrainFuck.smp @@ -0,0 +1,46 @@ +// -*- mode: java; -*- +val brain_fuck = { -> + var !program = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."; + 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; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc0.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc0.smp new file mode 100644 index 00000000..d85e39e5 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc0.smp @@ -0,0 +1 @@ +return new u8[arg]; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc2.smp new file mode 100644 index 00000000..864bcdc8 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc2.smp @@ -0,0 +1 @@ +int[] !xs = new int[3]; xs[arg]=1; return xs[arg&1]; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testArray1.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testArray1.smp new file mode 100644 index 00000000..7f3bd007 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testArray1.smp @@ -0,0 +1,8 @@ +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 diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBasic1.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBasic1.smp new file mode 100644 index 00000000..671e5b74 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBasic1.smp @@ -0,0 +1 @@ +return arg | 2; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBrainfuck.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBrainfuck.smp new file mode 100644 index 00000000..4e5ab81d --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBrainfuck.smp @@ -0,0 +1,43 @@ +u8[] !program = new u8[106];program[0] = 43;program[1] = 43;program[2] = 43;program[3] = 43;program[4] = 43;program[5] = 43;program[6] = 43;program[7] = 43;program[8] = 91;program[9] = 62;program[10] = 43;program[11] = 43;program[12] = 43;program[13] = 43;program[14] = 91;program[15] = 62;program[16] = 43;program[17] = 43;program[18] = 62;program[19] = 43;program[20] = 43;program[21] = 43;program[22] = 62;program[23] = 43;program[24] = 43;program[25] = 43;program[26] = 62;program[27] = 43;program[28] = 60;program[29] = 60;program[30] = 60;program[31] = 60;program[32] = 45;program[33] = 93;program[34] = 62;program[35] = 43;program[36] = 62;program[37] = 43;program[38] = 62;program[39] = 45;program[40] = 62;program[41] = 62;program[42] = 43;program[43] = 91;program[44] = 60;program[45] = 93;program[46] = 60;program[47] = 45;program[48] = 93;program[49] = 62;program[50] = 62;program[51] = 46;program[52] = 62;program[53] = 45;program[54] = 45;program[55] = 45;program[56] = 46;program[57] = 43;program[58] = 43;program[59] = 43;program[60] = 43;program[61] = 43;program[62] = 43;program[63] = 43;program[64] = 46;program[65] = 46;program[66] = 43;program[67] = 43;program[68] = 43;program[69] = 46;program[70] = 62;program[71] = 62;program[72] = 46;program[73] = 60;program[74] = 45;program[75] = 46;program[76] = 60;program[77] = 46;program[78] = 43;program[79] = 43;program[80] = 43;program[81] = 46;program[82] = 45;program[83] = 45;program[84] = 45;program[85] = 45;program[86] = 45;program[87] = 45;program[88] = 46;program[89] = 45;program[90] = 45;program[91] = 45;program[92] = 45;program[93] = 45;program[94] = 45;program[95] = 45;program[96] = 45;program[97] = 46;program[98] = 62;program[99] = 62;program[100] = 43;program[101] = 46;program[102] = 62;program[103] = 43;program[104] = 43;program[105] = 46; +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; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testCast.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testCast.smp new file mode 100644 index 00000000..d1368767 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testCast.smp @@ -0,0 +1 @@ +struct _Bar { int x; }; var b = arg ? new _Bar; return b ? b.x++ + b.x++ : -1; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags1.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags1.smp new file mode 100644 index 00000000..f630e959 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags1.smp @@ -0,0 +1,5 @@ +bool b1 = arg == 1; +bool b2 = arg == 2; +if (b2) if (b1) return 1; +if (b1) return 2; +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags2.smp new file mode 100644 index 00000000..70bfc226 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags2.smp @@ -0,0 +1,6 @@ +bool b1 = arg == 1; +while (arg > 0) { + arg--; + if (b1) arg--; +} +return arg; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFltArg.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFltArg.smp new file mode 100644 index 00000000..caa11f12 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFltArg.smp @@ -0,0 +1 @@ +return {int i, flt f, int j->return i+f+j;}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testMergeSort.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testMergeSort.smp new file mode 100644 index 00000000..417eba72 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testMergeSort.smp @@ -0,0 +1,46 @@ +// 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; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonFloat.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonFloat.smp new file mode 100644 index 00000000..fd76478f --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonFloat.smp @@ -0,0 +1,11 @@ +// 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) + _sqrt(farg+2.0); diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonInteger.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonInteger.smp new file mode 100644 index 00000000..8912dcd9 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonInteger.smp @@ -0,0 +1,10 @@ +// Newtons approximation to the square root +val _sqrt = { int x -> + int guess = x; + while( 1 ) { + int next = (x/guess + guess)/2; + if( next == guess ) return guess; + guess = next; + } +}; +return _sqrt(arg) + _sqrt(arg+2); diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testString.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testString.smp new file mode 100644 index 00000000..951cdc32 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testString.smp @@ -0,0 +1,35 @@ +struct _String { + u8[] cs; + new _String={ u8[] b -> cs=b; }; + 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(new u8[17]); +s.cs[0] = 67; // C +s.cs[1] = 108; // l +_hashCode(s); diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testArray1.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testArray1.smp new file mode 100644 index 00000000..6b10f92c --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testArray1.smp @@ -0,0 +1,9 @@ +// -*- mode: java; -*- +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 diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testNewtonFloat.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testNewtonFloat.smp new file mode 100644 index 00000000..58449f9c --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testNewtonFloat.smp @@ -0,0 +1,13 @@ +// -*- mode: java; -*- +// Newtons approximation to the square root +val _test_sqrt = { flt x -> + flt epsilon = 1e-15; + flt guess = x; + while( 1 ) { + flt next = (x/guess + guess)/2; + if( guess-epsilon <= next & next <= guess+epsilon ) return guess; + //if( guess==next ) return guess; + guess = next; + } +}; +flt farg = arg; return _test_sqrt(farg) + _test_sqrt(farg+2.0); diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testString.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testString.smp new file mode 100644 index 00000000..a18a5b88 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testString.smp @@ -0,0 +1,35 @@ +// -*- mode: java; -*- +struct _String { + u8[] cs; + int _hashCode; + new _String = { u8[] data -> cs=data; }; +}; + +// Compare two Strings +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; +}; + +// Return the String hashCode (cached, and never 0) +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; +}; + +// This fixture exports its API, not the last declaration as a result. +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testAntiDeps1.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testAntiDeps1.smp new file mode 100644 index 00000000..315cc5b6 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testAntiDeps1.smp @@ -0,0 +1,10 @@ +struct _S { int f; }; +var v0 = new _S; +!_S? !v1; +if (arg) v1 = new _S; +if (v1) { + v0.f = v1.f; +} else { + v0.f = 2; +} +return v0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount-2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount-2.smp new file mode 100644 index 00000000..20a05445 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount-2.smp @@ -0,0 +1,6 @@ +// -*- mode: java; -*- +val addAll = { int i0, flt f1, int i2, flt f3, int i4, flt f5, int i6, flt f7, int x8, flt f9, int i10, flt f11, int i12, flt f13, int i14, flt f15 -> + return + i0 + f1+ i2+ f3+ i4+ f5+ i6+ f7+ x8 +f9 + + i10+f11+i12+f13+i14+f15; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount.smp new file mode 100644 index 00000000..6b6bc0ae --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount.smp @@ -0,0 +1,6 @@ +// -*- mode: java; -*- +val addAll = { int i0, flt f1, int i2, flt f3, int i4, flt f5, int i6, flt f7, int x8, flt f9, int i10, flt f11, int i12, flt f13, int i14, flt f15, int x16, flt f17 int x18, flt f19 -> + return + i0 + f1+ i2+ f3+ i4+ f5+ i6+ f7+ x8 +f9 + + i10+f11+i12+f13+i14+f15+x16+f17+x18+f19 ; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testFibExport.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testFibExport.smp new file mode 100644 index 00000000..149872a7 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testFibExport.smp @@ -0,0 +1,10 @@ +val fib = {int n -> + int f1=1; + int f2=1; + while( n-- > 1 ){ + int temp = f1+f2; + f1=f2; + f2=temp; + } + return f2; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testInfinite.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testInfinite.smp new file mode 100644 index 00000000..1804ad59 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testInfinite.smp @@ -0,0 +1 @@ +struct _S { int i; }; !_S !s = new _S; while(1) s.i++; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testLoop2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testLoop2.smp new file mode 100644 index 00000000..cc54f790 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testLoop2.smp @@ -0,0 +1,9 @@ +int i = 0; +while(true) { + i += 1; + if( i==0 ) continue; + if( i==1 ) continue; + if( i==2 ) break; + continue; +} +return i; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testNewtonExport.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testNewtonExport.smp new file mode 100644 index 00000000..2786558e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testNewtonExport.smp @@ -0,0 +1,10 @@ +val test_sqrt = { flt x -> + flt epsilon = 1e-15; + flt guess = x; + while( 1 ) { + flt next = (x/guess + guess)/2; + if( guess-epsilon <= next & next <= guess+epsilon ) return guess; + //if( guess==next ) return guess; + guess = next; + } +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testPerson.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testPerson.smp new file mode 100644 index 00000000..09285034 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testPerson.smp @@ -0,0 +1,9 @@ +// -*-java-*- +struct Person { + int age; +}; + +val fcn = { !Person?[] ps, int x -> + if( ps[x] ) + ps[x].age++; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testSieve.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testSieve.smp new file mode 100644 index 00000000..beb0b3ad --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testSieve.smp @@ -0,0 +1,30 @@ +val sieve = { int N -> + // The main Sieve array + bool[] !ary = new bool[N]; + // The primes less than N + u32[] !primes = new u32[N>>1]; + // Number of primes so far, searching at index p + int nprimes = 0, p=2; + // Find primes while p^2 < N + while( p*p < N ) { + // 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; + p++; + } + + // Now just collect the remaining primes, no more marking + for( ; p < N; p++ ) + if( !ary[p] ) + primes[nprimes++] = p; + + // Copy/shrink the result array + u32[] !rez = new u32[nprimes]; + for( int j=0; j < nprimes; j++ ) + rez[j] = primes[j]; + return rez; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testStringExport.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testStringExport.smp new file mode 100644 index 00000000..ee5a0560 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testStringExport.smp @@ -0,0 +1,33 @@ +struct String { + u8[] cs; + int _hashCode; + new String = { u8[] data -> cs=data; }; +}; + +// Compare two Strings +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; +}; + +// Return the String hashCode (cached, and never 0) +val hashCode = { !String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + +val _hashCodeString = { !String self -> + int hash=0; + for( int i=0; i< self.cs#; i++ ) + hash = hash*31 + self.cs[i]; + if( !hash ) hash = 123456789; + return hash; +}; + +// This fixture exports its API, not the last declaration as a result. +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testHelloWorld.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testHelloWorld.smp new file mode 100644 index 00000000..1bcac36d --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testHelloWorld.smp @@ -0,0 +1,4 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +_p("Hello, World!"); +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testInfiniteReturn.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testInfiniteReturn.smp new file mode 100644 index 00000000..cc549808 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testInfiniteReturn.smp @@ -0,0 +1 @@ +struct _S { int i; }; !_S !s = new _S; while(1) s.i++; return s.i; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testNarrowCReturns.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testNarrowCReturns.smp new file mode 100644 index 00000000..21293a7a --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testNarrowCReturns.smp @@ -0,0 +1,12 @@ +{i8} c_i8="C"; {u8} c_u8="C"; +{i16} c_i16="C"; {u16} c_u16="C"; +{i32} c_i32="C"; {u32} c_u32="C"; +{int} c_i64="C"; +val test_i8={->return c_i8();}; +val test_u8={->return c_u8();}; +val test_i16={->return c_i16();}; +val test_u16={->return c_u16();}; +val test_i32={->return c_i32();}; +val test_u32={->return c_u32();}; +val test_i64={->return c_i64();}; +val test_cmp={->return c_i32()==-1;}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testPerson.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testPerson.smp new file mode 100644 index 00000000..f860091d --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testPerson.smp @@ -0,0 +1,8 @@ +struct Person { + i32 age; +}; + +val fcn = { !Person?[] ps, int x -> + if( ps[x] ) + ps[x].age++; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail.smp new file mode 100644 index 00000000..ec9c6d50 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail.smp @@ -0,0 +1,4 @@ +struct _Person { i32 age;}; +!_Person !p = new _Person; +p.age = (arg<<17)>>17; +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail2.smp new file mode 100644 index 00000000..d5264aa2 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail2.smp @@ -0,0 +1,5 @@ + + struct _Person { i32 age;}; + !_Person !p = new _Person; + p.age = (arg<<48)>>48; + return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextSuccess.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextSuccess.smp new file mode 100644 index 00000000..c4398a89 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextSuccess.smp @@ -0,0 +1,5 @@ +// Should fold away sign extend +struct _Person { i8 age;}; +!_Person !p = new _Person; +p.age = (arg<<48)>>48; +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23AllocTest-testString.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23AllocTest-testString.smp new file mode 100644 index 00000000..49c80a99 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23AllocTest-testString.smp @@ -0,0 +1,34 @@ +// -*- mode: java; -*- +struct _String { + u8[] cs; + int _hashCode; + new _String = { u8[] data -> cs=data; }; +}; + +// Compare two Strings +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; +}; + +// Return the String hashCode (cached, and never 0) +val hashCode = { !_String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + +val _hashCodeString = { !_String self -> + int hash=0; + for( int i=0; i< self.cs#; i++ ) + hash = hash*31 + self.cs[i]; + if( !hash ) hash = 123456789; + return hash; +}; + +// This fixture exports its API, not the last declaration as a result. +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAnd.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAnd.smp new file mode 100644 index 00000000..da85ef0e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAnd.smp @@ -0,0 +1,11 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int a = 1; +int b = 1; + +if(a && b) { + _p("And"); +} else { + _p("Or"); +} +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAndPtr.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAndPtr.smp new file mode 100644 index 00000000..a1e4e9ab --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAndPtr.smp @@ -0,0 +1,12 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +struct _S { !_S? !fld; new _S={ !_S? f -> fld=f; }; }; + +val ptr = arg == 1 ? null : new _S(arg==1 ? null : new _S(null)); +if( ptr && ptr.fld ) { + _p("true"); +} else { + _p("false"); +} + +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffAnd.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffAnd.smp new file mode 100644 index 00000000..1a6b5cf3 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffAnd.smp @@ -0,0 +1,17 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int a = 1; +int b = 1; + +int x=1; +int y=1; +int z=0; + +int g = x++ && y++ && z++; + +if(x == 2 && y == 2 && z == 1 && g == 0) { + _p("Effected"); +} else { + _p("Not effected"); +} +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffOr.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffOr.smp new file mode 100644 index 00000000..f2bb16c5 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffOr.smp @@ -0,0 +1,24 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int a = 1; +int b = 1; + +int x = -1; +int y=1; +int z= -1; + +int g = x++ || y++ || z++; + +int switch = 0; +if(x == 4 || y == 4 || z == 4) { + switch = -1; +} else { + switch = 1; +} +int cd = 1; +if((x == 0 && y == 1 && z==-1) && switch && g) { + _p("Effected"); +} else { + _p("Not effected"); +} +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFRefFields.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFRefFields.smp new file mode 100644 index 00000000..eac0a542 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFRefFields.smp @@ -0,0 +1,20 @@ +// A hypothetical scanner class +struct _Scan { + int !x; + u8[~] buf; + new _Scan={ u8[~] b -> buf=b; }; + // Skip whitespace + val skip = { -> + while( buf[x] <= ' ' ) + x++; + }; + // Peek a character; if matched consume it, else false. + val peek = { u8 c -> + skip(); + if( buf[x] != c ) return false; + x++; + return true; + }; +}; +!_Scan !s = new _Scan(" q"); +return s.peek('q'); diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFuncCall.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFuncCall.smp new file mode 100644 index 00000000..8a7b90d9 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFuncCall.smp @@ -0,0 +1,16 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +// -*- mode: java; -*- +int a = 1; +int b = 1; + +var _sq = { int x -> + x*x; +}; + +if(a && _sq(0)) { + _p("And"); +} else { + _p("Or"); +} +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testMethod.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testMethod.smp new file mode 100644 index 00000000..ced649e3 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testMethod.smp @@ -0,0 +1,17 @@ +// A hypothetical scanner class +struct _Scan { + int !x; + u8[~] buf; + new _Scan={ u8[~] b -> buf=b; }; + // Skip whitespace + val skip = { -> + while( buf[x] <= ' ' ) + x++; + }; + val require = { u8 ch -> + skip(); + buf[x++]==ch; + }; +}; +!_Scan !s = new _Scan(" [1,2]"); +return s.require('['); diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOr.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOr.smp new file mode 100644 index 00000000..dbab1ae5 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOr.smp @@ -0,0 +1,13 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int a = 1; +int b = 0; + +if(a++ || b++ ) { + if(b == 0 && a == 2) { + _p("Or"); + } +} else{ + _p("And"); +} +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopLocal.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopLocal.smp new file mode 100644 index 00000000..c4e4f9b5 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopLocal.smp @@ -0,0 +1,10 @@ +struct _S { int f; }; +int x=0; +int result=arg&1; +if(!result) { + !_S !s=new _S; + while(x<3) x++; + s.f=++x; + result=s.f; +} +return x*10+result; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopMemory.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopMemory.smp new file mode 100644 index 00000000..79478408 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopMemory.smp @@ -0,0 +1,11 @@ +struct _S { int f; }; +!_S !s=new _S; +int result=arg&1; +if(!result) { + !_S !t=new _S; + while(s.f<3) s.f++; + s.f++; + t.f=s.f; + result=t.f; +} +return s.f*10+result; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testBubbleSort.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testBubbleSort.smp new file mode 100644 index 00000000..f292254e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testBubbleSort.smp @@ -0,0 +1,217 @@ + + i32 errno = "C"; + // fd buf len -> len + { i32 i64 u32 -> u32 } write = "C"; + // fd buf len -> len + { i32 i64 u32 -> u32 } read = "C"; + // status + { i32 -> i32 } _exit = "C"; + // addr len prot flags fd off -> void* + { i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C"; + // mmap flags + i32 PROT_EXEC = 4; + i32 PROT_READ = 1; + i32 PROT_WRITE= 2; + i32 PROT_NONE = 0; + i32 MAP_PRIVATE = 2; + i32 MAP_ANON = 32; + + val _copy = { u8[] dst, int doff, u8[~] src, int soff, int len -> + for( int i=0; i<len; i++ ) + dst[i+doff] = src[i+soff]; + return dst; + }; + struct _aryu8 { + u32 !len = 0; + u8[] !buf; + new _aryu8={ u8[] b -> buf=b; }; + // Grow as needed + val _grow = { int len2 -> + if( len+len2 < buf# ) return self; + int newlen = buf# * 2; + while( len+len2 >= newlen ) newlen *= 2; + u8[] !buf2 = new u8[newlen]; + _copy(buf2,0,buf,0,buf#); + buf = buf2; + self; + }; + + // Add a character to aryu8 + val add = { u8 c -> + _grow(1).buf[len++] = c; + self; + }; + + // Add a plain array to the aryu8 + val addAry = { u8[~] ary -> + _grow(ary#); + _copy(buf,len,ary,0,ary#); + len += ary#; + self; + }; + + // Remove x chars from the tail. + val undo = { int x -> + len = len >= x ? len-x : 0; + self; + }; + + // Print ASCII number into aryu8 + val num = { i64 x -> + if( !x ) { add('0'); return self; } + if( x < 0 ) { add('-'); x = -x; } + i64 ten = 1, log = 0; + while( ten <= x ) { ten *= 10; log++; } + for( int i=0; i<log; i++ ) { + ten /= 10; + i64 q = x/ten; + u8 digit = q+'0'; + add(digit); + x -= q*ten; + } + self; + }; + + // Write to fd, returns the number of bytes written (len). + val write = { i32 fd -> i64 ptr = buf; write(fd,ptr,len); }; + }; +struct _Scan { + int !x; + u8[~] buf; + new _Scan={ u8[~] b -> buf=b; }; + + // Skip whitespace + val skip = { -> + while( buf[x] <= ' ' ) + x++; + return self; + }; + + // True if find required character, and skip it; else False. + val match = { u8 c -> + skip(); + if( buf[x] != c ) return false; + x++; + return true; + }; + + // Parse a simple number + val number = { -> + skip(); + bool neg = match('-'); + i64 n = 0; + while( '0' <= buf[x] & buf[x] <= '9' ) + n = n*10 + (buf[x++] - '0'); + return neg ? -n : n; + }; + }; +struct _aryi64 { + u32 !len = 0; + i64[] !buf; + new _aryi64={ i64[] b -> buf=b; }; + // Grow as needed + val _grow = { int len2 -> + if( len+len2 < buf# ) return self; + int newlen = buf# * 2; + while( len+len2 >= newlen ) newlen *= 2; + i64[] !buf2 = new i64[newlen]; + for( int i=0; i<buf#; i++ ) + buf2[i] = buf[i]; + buf = buf2; + self; + }; + + // Add a i64 to aryu8 + val add = { i64 c -> + _grow(1).buf[len++] = c; + self; + }; + + // Write to aryu8 + val str = { -> + // TODO: clunky default constructor syntax + var str = new _aryu8(new u8[1]).add('['); + for( int i=0; i<len; i++ ) + str.num(buf[i]).add(',').add(' '); + str.undo(2).add(']'); + }; + }; + val _parseAryI64 = { u8[~] str -> + !_Scan !s = new _Scan(str); + if( !s.match('[') ) return null; + // Array of parsed results + var nums = new _aryi64(new i64[1]); + // Parse a list of numbers + if( !s.match(']') ) { // Handle the empty list + // Parse "123, " until a something else + while( s.x < s.buf# ) { + nums.add(s.number()); + if( !s.match(',') ) + break; + }; + if( !s.match(']') ) return null; // Close the list + } + return nums; + }; + + + // Print string to stdout + val _p = { u8[~] str -> + i64 ptr = str; // cast array base to i64 + return write(1,ptr,str#); + }; + + // Return stdin as a u8[] + val _stdin = { -> + var buf = new u8[8*1024]; + i64 ptr = buf; + int len = 0; + while( true ) { + int rez = read(0,ptr,buf#); + if( rez < 0 ) _exit(errno); + len += rez; + if( rez < buf# ) + break; + _exit(-2); // Need to implement + } + // Compress + var dst = new u8[len]; + return _copy(dst,0,buf,0,len); + }; + // -*- mode: java; -*- + +// An implementation of +// https://sampleprograms.io/projects/bubble-sort/ +// +// Bubble sort, with reading (parsing) and writing (formatting) the data. + +val _sort = { !_aryi64 nums -> + var done = false; + while( !done ) { + done = true; + for( var i = 0; i < nums.len-1; i++ ) + if( nums.buf[i] > nums.buf[i+1] ) { + int tmp = nums.buf[i]; + nums.buf[i] = nums.buf[i+1]; + nums.buf[i+1] = tmp; // Swap + done = false; + } + } +}; + +// Read from stdin a line of the form "[ 4, 3, 2, 1]" and print it sorted. +val main = { -> + // Parse array of numbers from stdin + var nums = _parseAryI64( _stdin() ); + if( !nums ) { + _p("stdin must look like [4, 3, 2, 1]"); + return 1; + } + + // Bubble sort array of integers + _sort(nums); + + // Print a list of numbers to stdout + nums.str().write(1); + return 0; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testNewtonInteger.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testNewtonInteger.smp new file mode 100644 index 00000000..572e4ad7 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testNewtonInteger.smp @@ -0,0 +1,11 @@ +// Newtons approximation to the square root +val _sqrt = { int x -> + int guess = x; + while( 1 ) { + int next = (x/guess + guess)/2; + if( next == guess ) return guess; + guess = next; + } +}; +int cast_int = arg+2; +return _sqrt(arg) + _sqrt(cast_int); diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testStackArguments.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testStackArguments.smp new file mode 100644 index 00000000..b7d52dc5 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testStackArguments.smp @@ -0,0 +1,5 @@ +val addAll = { int i0, flt f1, int i2, flt f3, int i4, flt f5, int i6, flt f7, int x8, flt f9, int i10, flt f11, int i12, flt f13, int i14, flt f15, int x16, flt f17 int x18, flt f19 -> + return + i0 + f1+ i2+ f3+ i4+ f5+ i6+ f7+ x8 +f9 + + i10+f11+i12+f13+i14+f15+x16+f17+x18+f19 ; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack1.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack1.smp new file mode 100644 index 00000000..ded92426 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack1.smp @@ -0,0 +1,10 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int something_specific = 11; + +if (10 < something_specific < 20) { + _p("In Range"); +} else { + _p("Out of Range"); +} +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10-2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10-2.smp new file mode 100644 index 00000000..d9fb38d5 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10-2.smp @@ -0,0 +1 @@ +return (0 < 1) == (1>0); diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10.smp new file mode 100644 index 00000000..f92e5b00 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10.smp @@ -0,0 +1,3 @@ +var stack10 = { int x -> + (x < 1) == (1>x); +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11-2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11-2.smp new file mode 100644 index 00000000..b5aac70b --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11-2.smp @@ -0,0 +1 @@ +return 0 < arg < arg+1 < 4; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11.smp new file mode 100644 index 00000000..f50e3512 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11.smp @@ -0,0 +1,3 @@ +var stack11 = { int x -> + 0 < x < x+1 < 4; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12-2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12-2.smp new file mode 100644 index 00000000..575d1b4e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12-2.smp @@ -0,0 +1 @@ +return 0 != arg != 2; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12.smp new file mode 100644 index 00000000..b4f36c2e --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12.smp @@ -0,0 +1,3 @@ +var stack12 = { int x -> + 0 != x != 2; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13-2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13-2.smp new file mode 100644 index 00000000..9bde9687 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13-2.smp @@ -0,0 +1 @@ +return 0 == arg == 1; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13.smp new file mode 100644 index 00000000..ae7b38a3 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13.smp @@ -0,0 +1,3 @@ +var stack13 = { int x -> + 0 == x == 1; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack2.smp new file mode 100644 index 00000000..5590c42f --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack2.smp @@ -0,0 +1,10 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int score = 75; + +if (60 <= score < 90) { + _p("In Range"); +} else { +_p("Out of range"); +} +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack3.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack3.smp new file mode 100644 index 00000000..4700f8ce --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack3.smp @@ -0,0 +1,11 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +var _sq = { int x -> + x*x; +}; + +int score = 5; + +val str = (_sq(2) <= score < _sq(3)) ? "In Range" : "Out of range"; +_p(str); +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack4.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack4.smp new file mode 100644 index 00000000..47623b30 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack4.smp @@ -0,0 +1,10 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int something_specific = 9; + +if (10 < something_specific < 20) { + _p("In Range"); +} else { + _p("Out of Range"); +} +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack5.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack5.smp new file mode 100644 index 00000000..3c2c4208 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack5.smp @@ -0,0 +1,15 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +var _sq = { int x -> + x*x; +}; + +int score = 5; + +if (_sq(2) <= score < _sq(3)) { +_p("In Range"); +} else { +_p("Out of range"); +} + +return 0; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9-2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9-2.smp new file mode 100644 index 00000000..f54558f5 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9-2.smp @@ -0,0 +1 @@ +return (0 < 1) == 1; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9.smp new file mode 100644 index 00000000..15979be6 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9.smp @@ -0,0 +1,3 @@ +var stack9 = { int x -> + (x < 1) == 1; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/MergeSortTest-testMergeSort0.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/MergeSortTest-testMergeSort0.smp new file mode 100644 index 00000000..e6478eeb --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/MergeSortTest-testMergeSort0.smp @@ -0,0 +1,35 @@ + val merge_sort = { int[] a -> + int[] !b = new int[a#]; + copy_array (a, 0, a#, b); + split_merge(a, 0, a#, 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]; + }; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/README.ja.md b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/README.ja.md new file mode 100644 index 00000000..5ae019dd --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/README.ja.md @@ -0,0 +1,50 @@ +# 過去の割り当てコホート + +[English](README.md) | 日本語 + +この文書は [Historical allocation cohorts](README.md) の日本語訳です。 + +`cohorts.tsv` は、第24章の `SpillStats` で測定した212件のコンパイル項目を固定したものです。 +コホート20〜24の項目数は、それぞれ39、52、24、30、67です。 +ソースファイルは内容で重複を除き、最初に参照したテストの名前を付けています。 +マニフェストには、テストの識別情報、ターゲット、ABI、元の停止フェーズを保持しています。 +`Win64` は、第25章で使う ABI の表記 `win64` に変更しています。 +引数の型は i64、再実行のシードは全項目で123です。 + +これらの入力は、この最終監査で新たな正しさの回帰テストを追加する前に、 +第24章のテストから取得しました。比較のための安定したコーパスです。 +変更する場合は、比較可能性が失われる理由を説明する必要があります。 +人工的なアロケータの回帰テストや命令の回帰テストでの割り当ては、これらのコホートには含めません。 + +第25章では、以前のソースをすべて無変更で解析できるわけではありません。適応のための変更を明記します。 + +- String、Scan、連結された S のコンストラクタでは、`new S { field=...; }` を、 + 宣言されたコンストラクタとその引数に置き換えています。 + レシーバーを明示するスキャナ関数は、操作を保ったままインスタンスメソッドにしています。 +- 保存した `Chapter21Test-testStringExport`、`Chapter21AllocTest-testString`、 + `Chapter23AllocTest-testString` のソースは、必須の非 null フィールド `cs` を初期化するために、 + `new String = { u8[] data -> cs=data; };` を宣言しています。 + これによって10件の再実行項目(4件、3件、3件)を修正していますが、 + 等価性・ハッシュの本体やマニフェストの構成項目は変えていません。 + これらの適応済みソースが新しい比較基準です。以前のコンストラクタなしのフィクスチャと + 同一入力として、その集計値を比較することはできません。 +- 短絡評価の右辺で初期化する2つのループは、同等のガード付きブロックにしています。 + 割り当て、副作用、最終結果を保持しています。 +- 単純な I/O のケースには、以前の `write` の束縛と表示用の補助関数を直接含めています。 + Bubble Sort には、名前空間の接頭辞を平坦化し、コンストラクタを明示し、 + 置換用バッファを可変にした、以前のライブラリの本体を含めています。 + これにより、第25章で大幅に改訂した例に差し替えるのではなく、以前のワークロードを保持しています。 +- 元の Newton の浮動小数点数版2種類を含め、ほかのソース本文は保持しています。 + +すべての項目を **RegAlloc まで**再実行し、スケジューリング済みのレジスタ制約を確認します。 +過去のフェーズの列は来歴情報であり、第25章で以前のネイティブ実行ハーネスを動かすことを約束するものではありません。 +適応済み Bubble Sort は、割り当ての比較可能性のために、過去の `i32 errno="C"` 宣言を保持しています。 +直接のデータ束縛は現在、3つのターゲットすべてでエンコードできますが、 +errno がスレッドローカルまたはアクセサ経由の libc とは、この宣言をリンクできません。 +現在のシステムライブラリのコードは、代わりに `libc.errno()` を使います。 +このコーパスを、過去のランタイムの実行カバレッジとして数えてはいけません。 + +その後 `SpillStats` は、シード456で現在のシステムライブラリをエンコーディングまでコンパイルし、 +既存のシードとネイティブ検査を使って Chapter25Test を実行します。 +これらを合わせてコホート25とします。ライブラリまたはテストが失敗すると、レポートも失敗します。 +部分的な集計値では改善を示せません。現在のコンパイラの実行検証は、通常の章全体のテストスイートが担います。 diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/README.md b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/README.md new file mode 100644 index 00000000..b5ab9d80 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/README.md @@ -0,0 +1,78 @@ +# Historical allocation cohorts + +English | [日本語](README.ja.md) + +`cohorts.tsv` freezes 199 compilation entries matching Chapter 24's +`SpillStats`: 39, 52, 26, 24, and 58 entries for cohorts 20-24. The source files +are deduplicated by contents and named for their first referring test. The +manifest preserves test identity, target, ABI, and the original stopping phase. +`Win64` becomes `win64`, the Chapter 25 ABI spelling. Argument type is i64 and +the replay seed is 123 throughout. + +The original 212 entries were captured from the Chapter 24 tests before the +allocator audit's new correctness regressions. The 2026-10-07 refresh adds +Chapter22Test.testNarrowCReturns on x86 SystemV and Win64, matching the two +entries already measured in Chapters 22-24. The source is unchanged; only the +manifest's ABI spelling changes. The [cross-chapter matrix](../../../../../../../../docs/regalloc-spills.md) +checks matching test/CPU/ABI membership, including repeated compilations. +The same refresh removes all 15 Jig entries: six from cohort 23 and nine from +cohort 24. Jig methods are ignored scratch pads for debugging, not regression +tests or allocation benchmarks. Their frozen source copies were removed too. +These inputs are a stable comparison corpus; changing one +requires explaining the resulting break in comparability. Synthetic allocator +and instruction regressions do not contribute allocations to these cohorts. + +Chapter 25 cannot parse all earlier sources unchanged. Adaptations are explicit: + +- String, Scan, and linked S constructors replace `new S { field=...; }` with + declared constructors and arguments. The explicit-receiver scanner functions + become instance methods, preserving their operations. +- The saved `Chapter21Test-testStringExport`, `Chapter21AllocTest-testString`, + and `Chapter23AllocTest-testString` sources declare + `new String = { u8[] data -> cs=data; };` to initialize the required non-null + `cs` field. This repairs ten replay entries (four, three, and three), without + changing their equality/hash bodies or manifest membership. These adapted + sources are a new comparison baseline; their totals cannot be compared as + identical inputs with the earlier constructor-free fixtures. +- The two short-circuit RHS initializer loops become equivalent guarded blocks, + preserving allocation, side effects, and the final result. Their RHS assigns + the field value directly: Simple's `||` returns an operand, not a normalized + boolean. The 2026-10-07 cleanup removes an erroneous `!!` in those adaptations; + both fixtures return 44 for even arguments and 1 for odd arguments. +- Simple I/O cases include the old `write` binding and print helper directly. + Bubble Sort includes the old library bodies, with namespace prefixes flattened, + explicit constructors, and mutable replacement buffers. This retains the old + workload instead of substituting Chapter 25's substantially revised example. +- Local implementation names are private in eleven fixtures: the four Newton + variants that call their helper from the top level, Chapter 20's MergeSort, + String and Cast programs, and Chapter 22's three sign-extension programs and + InfiniteReturn. Their helpers/classes acquire an underscore prefix; operations, + inputs and final results are unchanged. This avoids exporting implementation + functions and allocation factories solely because Chapter 25 treats top-level + declarations as module members. Export/API fixtures, including NewtonExport, + StringExport, Person and the C-return wrappers, retain their public names. + These visibility adaptations change the generated workload and are recorded + separately from the allocator improvement in the cross-chapter comparison. +- The remaining visibility cleanup makes local classes/functions private in + AntiDeps1, Infinite, the Chapter 21/23 allocation String cases, the Chapter 23 + scanner/pointer/short-circuit/function cases, and Stack3/Stack5. Bubble Sort's + copied library implementation is private; its `main` stays public. The three + adapted String API fixtures end in `return 0;`, avoiding an accidental return + of the last declared function. Their equality/hash APIs stay public, and the + named StringExport fixture also keeps its public String factory. No cohort + entries, workload operations, or intentional export APIs are removed. +- Other source texts are retained. + +All entries replay **through RegAlloc**, checking scheduled register constraints. +The historical phase column is provenance, not a promise to run those old native +harnesses in Chapter 25. The adapted Bubble Sort retains its historical +`i32 errno="C"` declaration for allocation comparability. Direct data bindings +now encode on all three targets, but this declaration cannot link to a libc +whose errno is thread-local or accessor-based. Current system-library code uses +`libc.errno()` instead. Do not count this corpus as historical runtime coverage. + +`SpillStats` then compiles the current system library through encoding at seed +456, and runs Chapter25Test with its existing seeds and native checks. Together +these form cohort 25. A library or test failure makes the report fail; partial +totals cannot establish an improvement. The ordinary full chapter suite remains +the execution validation for the current compiler. diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/spill/cohorts.tsv b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/cohorts.tsv new file mode 100644 index 00000000..83b19d2a --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/spill/cohorts.tsv @@ -0,0 +1,200 @@ +# cohort test cpu abi original phase source +Chapter20 Chapter20Test.testMergeSort x86_64_v2 SystemV RegAlloc Chapter20Test-testMergeSort.smp +Chapter20 Chapter20Test.testMergeSort riscv SystemV RegAlloc Chapter20Test-testMergeSort.smp +Chapter20 Chapter20Test.testMergeSort arm SystemV RegAlloc Chapter20Test-testMergeSort.smp +Chapter20 Chapter20Test.testNewtonInteger x86_64_v2 SystemV RegAlloc Chapter20Test-testNewtonInteger.smp +Chapter20 Chapter20Test.testNewtonInteger riscv SystemV RegAlloc Chapter20Test-testNewtonInteger.smp +Chapter20 Chapter20Test.testNewtonInteger arm SystemV RegAlloc Chapter20Test-testNewtonInteger.smp +Chapter20 Chapter20Test.testCast x86_64_v2 SystemV RegAlloc Chapter20Test-testCast.smp +Chapter20 Chapter20Test.testCast riscv SystemV RegAlloc Chapter20Test-testCast.smp +Chapter20 Chapter20Test.testCast arm SystemV RegAlloc Chapter20Test-testCast.smp +Chapter20 Chapter20Test.testNewtonFloat x86_64_v2 SystemV RegAlloc Chapter20Test-testNewtonFloat.smp +Chapter20 Chapter20Test.testNewtonFloat riscv SystemV RegAlloc Chapter20Test-testNewtonFloat.smp +Chapter20 Chapter20Test.testNewtonFloat arm SystemV RegAlloc Chapter20Test-testNewtonFloat.smp +Chapter20 Chapter20Test.testBrainfuck x86_64_v2 SystemV RegAlloc Chapter20Test-testBrainfuck.smp +Chapter20 Chapter20Test.testBrainfuck riscv SystemV RegAlloc Chapter20Test-testBrainfuck.smp +Chapter20 Chapter20Test.testBrainfuck arm SystemV RegAlloc Chapter20Test-testBrainfuck.smp +Chapter20 Chapter20Test.testAlloc0 x86_64_v2 SystemV RegAlloc Chapter20Test-testAlloc0.smp +Chapter20 Chapter20Test.testAlloc0 riscv SystemV RegAlloc Chapter20Test-testAlloc0.smp +Chapter20 Chapter20Test.testAlloc0 arm SystemV RegAlloc Chapter20Test-testAlloc0.smp +Chapter20 Chapter20Test.testAlloc2 x86_64_v2 SystemV RegAlloc Chapter20Test-testAlloc2.smp +Chapter20 Chapter20Test.testAlloc2 riscv SystemV RegAlloc Chapter20Test-testAlloc2.smp +Chapter20 Chapter20Test.testAlloc2 arm SystemV RegAlloc Chapter20Test-testAlloc2.smp +Chapter20 Chapter20Test.testArray1 x86_64_v2 SystemV RegAlloc Chapter20Test-testArray1.smp +Chapter20 Chapter20Test.testArray1 riscv SystemV RegAlloc Chapter20Test-testArray1.smp +Chapter20 Chapter20Test.testArray1 arm SystemV RegAlloc Chapter20Test-testArray1.smp +Chapter20 Chapter20Test.testBasic1 x86_64_v2 SystemV RegAlloc Chapter20Test-testBasic1.smp +Chapter20 Chapter20Test.testBasic1 riscv SystemV RegAlloc Chapter20Test-testBasic1.smp +Chapter20 Chapter20Test.testBasic1 arm SystemV RegAlloc Chapter20Test-testBasic1.smp +Chapter20 Chapter20Test.testFlags1 x86_64_v2 SystemV RegAlloc Chapter20Test-testFlags1.smp +Chapter20 Chapter20Test.testFlags1 riscv SystemV RegAlloc Chapter20Test-testFlags1.smp +Chapter20 Chapter20Test.testFlags1 arm SystemV RegAlloc Chapter20Test-testFlags1.smp +Chapter20 Chapter20Test.testFlags2 x86_64_v2 SystemV RegAlloc Chapter20Test-testFlags2.smp +Chapter20 Chapter20Test.testFlags2 riscv SystemV RegAlloc Chapter20Test-testFlags2.smp +Chapter20 Chapter20Test.testFlags2 arm SystemV RegAlloc Chapter20Test-testFlags2.smp +Chapter20 Chapter20Test.testFltArg x86_64_v2 SystemV RegAlloc Chapter20Test-testFltArg.smp +Chapter20 Chapter20Test.testFltArg riscv SystemV RegAlloc Chapter20Test-testFltArg.smp +Chapter20 Chapter20Test.testFltArg arm SystemV RegAlloc Chapter20Test-testFltArg.smp +Chapter20 Chapter20Test.testString x86_64_v2 SystemV RegAlloc Chapter20Test-testString.smp +Chapter20 Chapter20Test.testString riscv SystemV RegAlloc Chapter20Test-testString.smp +Chapter20 Chapter20Test.testString arm SystemV RegAlloc Chapter20Test-testString.smp +Chapter21 Chapter21Test.testFibExport x86_64_v2 win64 Export Chapter21Test-testFibExport.smp +Chapter21 Chapter21Test.testFibExport riscv SystemV Export Chapter21Test-testFibExport.smp +Chapter21 Chapter21Test.testFibExport arm SystemV Export Chapter21Test-testFibExport.smp +Chapter21 Chapter21Test.testAntiDeps1 x86_64_v2 SystemV Encoding Chapter21Test-testAntiDeps1.smp +Chapter21 Chapter21Test.testAntiDeps1 riscv SystemV Encoding Chapter21Test-testAntiDeps1.smp +Chapter21 Chapter21Test.testAntiDeps1 arm SystemV Encoding Chapter21Test-testAntiDeps1.smp +Chapter21 Chapter21Test.testLoop2 x86_64_v2 win64 Encoding Chapter21Test-testLoop2.smp +Chapter21 Chapter21Test.testLoop2 riscv SystemV Encoding Chapter21Test-testLoop2.smp +Chapter21 Chapter21Test.testLoop2 arm SystemV Encoding Chapter21Test-testLoop2.smp +Chapter21 Chapter21Test.testSieve x86_64_v2 win64 Export Chapter21Test-testSieve.smp +Chapter21 Chapter21Test.testSieve riscv SystemV Export Chapter21Test-testSieve.smp +Chapter21 Chapter21Test.testSieve arm SystemV Export Chapter21Test-testSieve.smp +Chapter21 Chapter21Test.testArgCount x86_64_v2 win64 Export Chapter21Test-testArgCount.smp +Chapter21 Chapter21Test.testArgCount riscv SystemV Export Chapter21Test-testArgCount-2.smp +Chapter21 Chapter21Test.testArgCount arm SystemV Export Chapter21Test-testArgCount-2.smp +Chapter21 Chapter21Test.testStringExport x86_64_v2 win64 Export Chapter21Test-testStringExport.smp +Chapter21 Chapter21Test.testInfinite x86_64_v2 SystemV Encoding Chapter21Test-testInfinite.smp +Chapter21 Chapter21Test.testInfinite riscv SystemV Encoding Chapter21Test-testInfinite.smp +Chapter21 Chapter21Test.testInfinite arm SystemV Encoding Chapter21Test-testInfinite.smp +Chapter21 Chapter21Test.testArray1 x86_64_v2 SystemV Encoding Chapter20Test-testArray1.smp +Chapter21 Chapter21Test.testArray1 riscv SystemV Encoding Chapter20Test-testArray1.smp +Chapter21 Chapter21Test.testArray1 arm SystemV Encoding Chapter20Test-testArray1.smp +Chapter21 Chapter21Test.testBasic1 x86_64_v2 SystemV Encoding Chapter20Test-testBasic1.smp +Chapter21 Chapter21Test.testBasic1 riscv SystemV Encoding Chapter20Test-testBasic1.smp +Chapter21 Chapter21Test.testBasic1 arm SystemV Encoding Chapter20Test-testBasic1.smp +Chapter21 Chapter21Test.testNewtonExport x86_64_v2 win64 Export Chapter21Test-testNewtonExport.smp +Chapter21 Chapter21Test.testNewtonExport riscv SystemV Export Chapter21Test-testNewtonExport.smp +Chapter21 Chapter21Test.testNewtonExport arm SystemV Export Chapter21Test-testNewtonExport.smp +Chapter21 Chapter21Test.testPerson x86_64_v2 win64 Export Chapter21Test-testPerson.smp +Chapter21 Chapter21Test.testPerson riscv SystemV Export Chapter21Test-testPerson.smp +Chapter21 Chapter21Test.testPerson arm SystemV Export Chapter21Test-testPerson.smp +Chapter21 Chapter21Test.testString x86_64_v2 SystemV Encoding Chapter21Test-testStringExport.smp +Chapter21 Chapter21Test.testString riscv SystemV Encoding Chapter21Test-testStringExport.smp +Chapter21 Chapter21Test.testString arm SystemV Encoding Chapter21Test-testStringExport.smp +Chapter21 Chapter21AllocTest.testNewtonInteger x86_64_v2 win64 Encoding Chapter20Test-testNewtonInteger.smp +Chapter21 Chapter21AllocTest.testNewtonInteger riscv SystemV Encoding Chapter20Test-testNewtonInteger.smp +Chapter21 Chapter21AllocTest.testNewtonInteger arm SystemV Encoding Chapter20Test-testNewtonInteger.smp +Chapter21 Chapter21AllocTest.testNewtonFloat x86_64_v2 SystemV Encoding Chapter21AllocTest-testNewtonFloat.smp +Chapter21 Chapter21AllocTest.testNewtonFloat riscv SystemV Encoding Chapter21AllocTest-testNewtonFloat.smp +Chapter21 Chapter21AllocTest.testNewtonFloat arm SystemV Encoding Chapter21AllocTest-testNewtonFloat.smp +Chapter21 Chapter21AllocTest.testArray1 x86_64_v2 SystemV Encoding Chapter21AllocTest-testArray1.smp +Chapter21 Chapter21AllocTest.testArray1 riscv SystemV Encoding Chapter21AllocTest-testArray1.smp +Chapter21 Chapter21AllocTest.testArray1 arm SystemV Encoding Chapter21AllocTest-testArray1.smp +Chapter21 Chapter21AllocTest.testString x86_64_v2 SystemV Encoding Chapter21AllocTest-testString.smp +Chapter21 Chapter21AllocTest.testString riscv SystemV Encoding Chapter21AllocTest-testString.smp +Chapter21 Chapter21AllocTest.testString arm SystemV Encoding Chapter21AllocTest-testString.smp +Chapter22 Chapter22Test.testSextFail x86_64_v2 win64 Encoding Chapter22Test-testSextFail.smp +Chapter22 Chapter22Test.testSextFail riscv SystemV Encoding Chapter22Test-testSextFail.smp +Chapter22 Chapter22Test.testSextFail arm SystemV Encoding Chapter22Test-testSextFail.smp +Chapter22 Chapter22Test.testSextFail riscv SystemV Export Chapter22Test-testSextFail.smp +Chapter22 Chapter22Test.testSextFail arm SystemV Export Chapter22Test-testSextFail.smp +Chapter22 Chapter22Test.testSextSuccess riscv SystemV Export Chapter22Test-testSextSuccess.smp +Chapter22 Chapter22Test.testSextSuccess arm SystemV Export Chapter22Test-testSextSuccess.smp +Chapter22 Chapter22Test.testSextSuccess x86_64_v2 win64 Encoding Chapter22Test-testSextSuccess.smp +Chapter22 Chapter22Test.testSextSuccess riscv SystemV Encoding Chapter22Test-testSextSuccess.smp +Chapter22 Chapter22Test.testSextSuccess arm SystemV Encoding Chapter22Test-testSextSuccess.smp +Chapter22 Chapter22Test.testInfiniteReturn x86_64_v2 SystemV Encoding Chapter22Test-testInfiniteReturn.smp +Chapter22 Chapter22Test.testInfiniteReturn riscv SystemV Encoding Chapter22Test-testInfiniteReturn.smp +Chapter22 Chapter22Test.testInfiniteReturn arm SystemV Encoding Chapter22Test-testInfiniteReturn.smp +Chapter22 Chapter22Test.testSextFail2 riscv SystemV Export Chapter22Test-testSextFail2.smp +Chapter22 Chapter22Test.testSextFail2 arm SystemV Export Chapter22Test-testSextFail2.smp +Chapter22 Chapter22Test.testSextFail2 x86_64_v2 win64 Encoding Chapter22Test-testSextFail2.smp +Chapter22 Chapter22Test.testSextFail2 riscv SystemV Encoding Chapter22Test-testSextFail2.smp +Chapter22 Chapter22Test.testSextFail2 arm SystemV Encoding Chapter22Test-testSextFail2.smp +Chapter22 Chapter22Test.testPerson x86_64_v2 win64 Export Chapter22Test-testPerson.smp +Chapter22 Chapter22Test.testPerson riscv SystemV Export Chapter22Test-testPerson.smp +Chapter22 Chapter22Test.testPerson arm SystemV Export Chapter22Test-testPerson.smp +Chapter22 Chapter22Test.testHelloWorld x86_64_v2 win64 Export Chapter22Test-testHelloWorld.smp +Chapter22 Chapter22Test.testHelloWorld riscv SystemV Export Chapter22Test-testHelloWorld.smp +Chapter22 Chapter22Test.testHelloWorld arm SystemV Export Chapter22Test-testHelloWorld.smp +Chapter23 Chapter23Test.testAnd x86_64_v2 win64 Export Chapter23Test-testAnd.smp +Chapter23 Chapter23Test.testAnd riscv SystemV Export Chapter23Test-testAnd.smp +Chapter23 Chapter23Test.testAnd arm SystemV Export Chapter23Test-testAnd.smp +Chapter23 Chapter23Test.testOr x86_64_v2 win64 Export Chapter23Test-testOr.smp +Chapter23 Chapter23Test.testOr riscv SystemV Export Chapter23Test-testOr.smp +Chapter23 Chapter23Test.testOr arm SystemV Export Chapter23Test-testOr.smp +Chapter23 Chapter23Test.testCondSideEffAnd x86_64_v2 win64 Export Chapter23Test-testCondSideEffAnd.smp +Chapter23 Chapter23Test.testCondSideEffAnd riscv SystemV Export Chapter23Test-testCondSideEffAnd.smp +Chapter23 Chapter23Test.testCondSideEffAnd arm SystemV Export Chapter23Test-testCondSideEffAnd.smp +Chapter23 Chapter23Test.testFRefFields x86_64_v2 win64 Encoding Chapter23Test-testFRefFields.smp +Chapter23 Chapter23Test.testAndPtr riscv SystemV Export Chapter23Test-testAndPtr.smp +Chapter23 Chapter23Test.testAndPtr arm SystemV Export Chapter23Test-testAndPtr.smp +Chapter23 Chapter23Test.testOrRhsLoopLocal riscv SystemV Export Chapter23Test-testOrRhsLoopLocal.smp +Chapter23 Chapter23Test.testMethod x86_64_v2 win64 Encoding Chapter23Test-testMethod.smp +Chapter23 Chapter23Test.testOrRhsLoopMemory riscv SystemV Export Chapter23Test-testOrRhsLoopMemory.smp +Chapter23 Chapter23Test.testCondSideEffOr x86_64_v2 win64 Export Chapter23Test-testCondSideEffOr.smp +Chapter23 Chapter23Test.testCondSideEffOr riscv SystemV Export Chapter23Test-testCondSideEffOr.smp +Chapter23 Chapter23Test.testCondSideEffOr arm SystemV Export Chapter23Test-testCondSideEffOr.smp +Chapter23 Chapter23Test.testFuncCall x86_64_v2 win64 Export Chapter23Test-testFuncCall.smp +Chapter23 Chapter23Test.testFuncCall riscv SystemV Export Chapter23Test-testFuncCall.smp +Chapter23 Chapter23Test.testFuncCall arm SystemV Export Chapter23Test-testFuncCall.smp +Chapter23 Chapter23AllocTest.testString x86_64_v2 SystemV Encoding Chapter23AllocTest-testString.smp +Chapter23 Chapter23AllocTest.testString riscv SystemV Encoding Chapter23AllocTest-testString.smp +Chapter23 Chapter23AllocTest.testString arm SystemV Encoding Chapter23AllocTest-testString.smp +Chapter24 Chapter24Test.testAnd x86_64_v2 win64 Export Chapter23Test-testAnd.smp +Chapter24 Chapter24Test.testAnd riscv SystemV Export Chapter23Test-testAnd.smp +Chapter24 Chapter24Test.testAnd arm SystemV Export Chapter23Test-testAnd.smp +Chapter24 Chapter24Test.testOr x86_64_v2 win64 Export Chapter23Test-testOr.smp +Chapter24 Chapter24Test.testOr riscv SystemV Export Chapter23Test-testOr.smp +Chapter24 Chapter24Test.testOr arm SystemV Export Chapter23Test-testOr.smp +Chapter24 Chapter24Test.testCondSideEffAnd x86_64_v2 win64 Export Chapter23Test-testCondSideEffAnd.smp +Chapter24 Chapter24Test.testCondSideEffAnd riscv SystemV Export Chapter23Test-testCondSideEffAnd.smp +Chapter24 Chapter24Test.testCondSideEffAnd arm SystemV Export Chapter23Test-testCondSideEffAnd.smp +Chapter24 Chapter24Test.testFRefFields x86_64_v2 win64 Encoding Chapter23Test-testFRefFields.smp +Chapter24 Chapter24Test.testAndPtr riscv SystemV Export Chapter23Test-testAndPtr.smp +Chapter24 Chapter24Test.testAndPtr arm SystemV Export Chapter23Test-testAndPtr.smp +Chapter24 Chapter24Test.testMethod x86_64_v2 win64 Encoding Chapter23Test-testMethod.smp +Chapter24 Chapter24Test.testStack1 x86_64_v2 win64 Export Chapter24Test-testStack1.smp +Chapter24 Chapter24Test.testStack1 riscv SystemV Export Chapter24Test-testStack1.smp +Chapter24 Chapter24Test.testStack1 arm SystemV Export Chapter24Test-testStack1.smp +Chapter24 Chapter24Test.testStack2 x86_64_v2 win64 Export Chapter24Test-testStack2.smp +Chapter24 Chapter24Test.testStack2 riscv SystemV Export Chapter24Test-testStack2.smp +Chapter24 Chapter24Test.testStack2 arm SystemV Export Chapter24Test-testStack2.smp +Chapter24 Chapter24Test.testStack3 x86_64_v2 win64 Export Chapter24Test-testStack3.smp +Chapter24 Chapter24Test.testStack3 riscv SystemV Export Chapter24Test-testStack3.smp +Chapter24 Chapter24Test.testStack3 arm SystemV Export Chapter24Test-testStack3.smp +Chapter24 Chapter24Test.testStack4 x86_64_v2 win64 Export Chapter24Test-testStack4.smp +Chapter24 Chapter24Test.testStack4 riscv SystemV Export Chapter24Test-testStack4.smp +Chapter24 Chapter24Test.testStack4 arm SystemV Export Chapter24Test-testStack4.smp +Chapter24 Chapter24Test.testStack5 x86_64_v2 win64 Export Chapter24Test-testStack5.smp +Chapter24 Chapter24Test.testStack5 riscv SystemV Export Chapter24Test-testStack5.smp +Chapter24 Chapter24Test.testStack5 arm SystemV Export Chapter24Test-testStack5.smp +Chapter24 Chapter24Test.testStack9 x86_64_v2 win64 Export Chapter24Test-testStack9.smp +Chapter24 Chapter24Test.testStack9 riscv SystemV Export Chapter24Test-testStack9-2.smp +Chapter24 Chapter24Test.testStack9 arm SystemV Export Chapter24Test-testStack9-2.smp +Chapter24 Chapter24Test.testCondSideEffOr x86_64_v2 win64 Export Chapter23Test-testCondSideEffOr.smp +Chapter24 Chapter24Test.testCondSideEffOr riscv SystemV Export Chapter23Test-testCondSideEffOr.smp +Chapter24 Chapter24Test.testCondSideEffOr arm SystemV Export Chapter23Test-testCondSideEffOr.smp +Chapter24 Chapter24Test.testStack10 x86_64_v2 win64 Export Chapter24Test-testStack10.smp +Chapter24 Chapter24Test.testStack10 riscv SystemV Export Chapter24Test-testStack10-2.smp +Chapter24 Chapter24Test.testStack10 arm SystemV Export Chapter24Test-testStack10-2.smp +Chapter24 Chapter24Test.testStack11 x86_64_v2 win64 Export Chapter24Test-testStack11.smp +Chapter24 Chapter24Test.testStack11 riscv SystemV Export Chapter24Test-testStack11-2.smp +Chapter24 Chapter24Test.testStack11 arm SystemV Export Chapter24Test-testStack11-2.smp +Chapter24 Chapter24Test.testStack12 x86_64_v2 win64 Export Chapter24Test-testStack12.smp +Chapter24 Chapter24Test.testStack12 riscv SystemV Export Chapter24Test-testStack12-2.smp +Chapter24 Chapter24Test.testStack12 arm SystemV Export Chapter24Test-testStack12-2.smp +Chapter24 Chapter24Test.testStack13 x86_64_v2 win64 Export Chapter24Test-testStack13.smp +Chapter24 Chapter24Test.testStack13 riscv SystemV Export Chapter24Test-testStack13-2.smp +Chapter24 Chapter24Test.testStack13 arm SystemV Export Chapter24Test-testStack13-2.smp +Chapter24 Chapter24Test.testFuncCall x86_64_v2 win64 Export Chapter23Test-testFuncCall.smp +Chapter24 Chapter24Test.testFuncCall riscv SystemV Export Chapter23Test-testFuncCall.smp +Chapter24 Chapter24Test.testFuncCall arm SystemV Export Chapter23Test-testFuncCall.smp +Chapter24 Chapter24AllocTest.testNewtonInteger x86_64_v2 win64 Encoding Chapter24AllocTest-testNewtonInteger.smp +Chapter24 Chapter24AllocTest.testNewtonInteger riscv SystemV Encoding Chapter24AllocTest-testNewtonInteger.smp +Chapter24 Chapter24AllocTest.testNewtonInteger arm SystemV Encoding Chapter24AllocTest-testNewtonInteger.smp +Chapter24 Chapter24AllocTest.testStackArguments x86_64_v2 win64 Export Chapter24AllocTest-testStackArguments.smp +Chapter24 Chapter24AllocTest.testStackArguments riscv SystemV Export Chapter24AllocTest-testStackArguments.smp +Chapter24 Chapter24AllocTest.testStackArguments arm SystemV Export Chapter24AllocTest-testStackArguments.smp +Chapter24 Chapter24AllocTest.testBubbleSort x86_64_v2 win64 Encoding Chapter24AllocTest-testBubbleSort.smp +Chapter24 Chapter24AllocTest.testBubbleSort riscv SystemV Encoding Chapter24AllocTest-testBubbleSort.smp +Chapter24 Chapter24AllocTest.testBubbleSort arm SystemV Encoding Chapter24AllocTest-testBubbleSort.smp +Chapter21 BrainFuckTest.testBrainFuck x86_64_v2 win64 Export BrainFuckTest-testBrainFuck.smp +Chapter21 BrainFuckTest.testBrainFuck riscv SystemV Export BrainFuckTest-testBrainFuck.smp +Chapter21 BrainFuckTest.testBrainFuck arm SystemV Export BrainFuckTest-testBrainFuck.smp +Chapter21 MergeSortTest.testMergeSort0 x86_64_v2 win64 Export MergeSortTest-testMergeSort0.smp +Chapter21 MergeSortTest.testMergeSort0 riscv SystemV Export MergeSortTest-testMergeSort0.smp +Chapter21 MergeSortTest.testMergeSort0 arm SystemV Encoding MergeSortTest-testMergeSort0.smp +Chapter22 Chapter22Test.testNarrowCReturns x86_64_v2 SystemV Export Chapter22Test-testNarrowCReturns.smp +Chapter22 Chapter22Test.testNarrowCReturns x86_64_v2 win64 Export Chapter22Test-testNarrowCReturns.smp diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test0/A.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test0/A.smp new file mode 100644 index 00000000..b94fa147 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test0/A.smp @@ -0,0 +1,6 @@ +// -*- mode: java; -*- + +val _fcn = { -> arg ? { int x -> x+1; } : { int x -> x-1; }; }; + +// Refer to child B is OK because last field of A has been initialized +_fcn()(B.x); diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test0/A/B.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test0/A/B.smp new file mode 100644 index 00000000..82844f74 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test0/A/B.smp @@ -0,0 +1 @@ +val x=5; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test5/par.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test5/par.smp new file mode 100644 index 00000000..f19bd57b --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test5/par.smp @@ -0,0 +1,20 @@ +// -*- mode: java; -*- + +// Parent module +val par0 = 3; // Some final field + +// Nested class in parent. +// Makes a field: "val child0 = ..." +struct child0 { + val kid0 = 5; +}; + +// Nested class in parent, comes from disk. +// These are defined in-order +val child1 = child1; + +// Nested class in parent, comes from disk +val child2 = child2; + +// Nested class in parent, comes from disk +val child3 = child3; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test5/par/child1.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test5/par/child1.smp new file mode 100644 index 00000000..7bca0cf7 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test5/par/child1.smp @@ -0,0 +1,12 @@ +// -*- mode: java; -*- + +// Child module + +// Same field name as child2 +val kid1 = 7; + +// Can read parent directly +val kid2 = par0; + +// Error to reference a later sibling +// val kid3 = par.child2.kid1; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test5/par/child2.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test5/par/child2.smp new file mode 100644 index 00000000..ab5e2ac1 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test5/par/child2.smp @@ -0,0 +1,13 @@ +// -*- mode: java; -*- + +// Child module + +// Same field name as child2 +val kid1 = 11; + +// Can read parent directly. +// Can read defined-in-parent fields. +val kid2 = child0.kid0; + +// OK to reference a past sibling +val kid3 = child1.kid1; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m.smp new file mode 100644 index 00000000..569f38be --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m.smp @@ -0,0 +1,2 @@ +val a = a; +val b = b; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/a.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/a.smp new file mode 100644 index 00000000..97113b71 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/a.smp @@ -0,0 +1 @@ +val f = { -> new b(1); }; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/b.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/b.smp new file mode 100644 index 00000000..43770b60 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/b.smp @@ -0,0 +1,4 @@ +struct b { + int x; + new b = { int y -> x = y; }; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys.smp new file mode 100644 index 00000000..1725c57d --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys.smp @@ -0,0 +1,3 @@ +// Reduced multi-compilation-unit reproducer for order-sensitive inlining. +var aryu8 = aryu8; +var aryi64 = aryi64; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/ary.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/ary.smp new file mode 100644 index 00000000..e3dd6140 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/ary.smp @@ -0,0 +1,6 @@ +// Array utilities, discovered indirectly from aryu8. +val copy = { u8[] dst, int doff, u8[~] src, int soff, int len -> + for( int i=0; i<len; i++ ) + dst[i+doff] = src[i+soff]; + return dst; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryi64.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryi64.smp new file mode 100644 index 00000000..53c93666 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryi64.smp @@ -0,0 +1,29 @@ +struct aryi64 { + u32 !len = 0; + i64[] !buf; + + new aryi64 = { i64[] b -> buf = b; }; + + val _grow = { int len2 -> + if( len+len2 < buf# ) return self; + int newlen = buf# * 2; + while( len+len2 >= newlen ) newlen *= 2; + i64[] !buf2 = new i64[newlen]; + for( int i=0; i<buf#; i++ ) + buf2[i] = buf[i]; + buf = buf2; + self; + }; + + val add = { i64 c -> + _grow(1).buf[len++] = c; + self; + }; + + val str = { -> + var str = new aryu8(new u8[1]).add('['); + for( int i=0; i<len; i++ ) + str.num(buf[i]).add(',').add(' '); + str.undo(2).add(']'); + }; +}; diff --git a/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryu8.smp b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryu8.smp new file mode 100644 index 00000000..e6b708a4 --- /dev/null +++ b/chapter-modules/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryu8.smp @@ -0,0 +1,50 @@ +struct aryu8 { + u32 !len = 0; + u8[] !buf; + + new aryu8 = { u8[] b -> buf = b; }; + + val _grow = { int len2 -> + if( len+len2 < buf# ) return self; + int newlen = buf# * 2; + while( len+len2 >= newlen ) newlen *= 2; + u8[] !buf2 = new u8[newlen]; + ary.copy(buf2,0,buf,0,buf#); + buf = buf2; + self; + }; + + val clear = { -> len = 0; self; }; + + val add = { u8 c -> + _grow(1).buf[len++] = c; + self; + }; + + val addAry = { u8[~] xs -> + _grow(xs#); + ary.copy(buf,len,xs,0,xs#); + len += xs#; + self; + }; + + val undo = { int x -> + len = len >= x ? len-x : 0; + self; + }; + + val num = { i64 x -> + if( !x ) { add('0'); return self; } + if( x < 0 ) { add('-'); x = -x; } + i64 ten = 1, log = 0; + while( ten <= x ) { ten *= 10; log++; } + for( int i=0; i<log; i++ ) { + ten /= 10; + i64 q = x/ten; + u8 digit = q+'0'; + add(digit); + x -= q*ten; + } + self; + }; +}; diff --git a/chapter-modules/src/test/smp/lazy/App.smp b/chapter-modules/src/test/smp/lazy/App.smp new file mode 100644 index 00000000..caf7b0c1 --- /dev/null +++ b/chapter-modules/src/test/smp/lazy/App.smp @@ -0,0 +1,3 @@ +val Main=Main; +val Tables=Tables; +val Stats=Stats; diff --git a/chapter-modules/src/test/smp/lazy/App/Main.smp b/chapter-modules/src/test/smp/lazy/App/Main.smp new file mode 100644 index 00000000..2444f21b --- /dev/null +++ b/chapter-modules/src/test/smp/lazy/App/Main.smp @@ -0,0 +1 @@ +return arg ? Tables.square(3)+Tables.square(4)+100*Stats.builds : Stats.builds; diff --git a/chapter-modules/src/test/smp/lazy/App/Stats.smp b/chapter-modules/src/test/smp/lazy/App/Stats.smp new file mode 100644 index 00000000..e2d9e02e --- /dev/null +++ b/chapter-modules/src/test/smp/lazy/App/Stats.smp @@ -0,0 +1 @@ +int builds=0; diff --git a/chapter-modules/src/test/smp/lazy/App/Tables.smp b/chapter-modules/src/test/smp/lazy/App/Tables.smp new file mode 100644 index 00000000..0f32a118 --- /dev/null +++ b/chapter-modules/src/test/smp/lazy/App/Tables.smp @@ -0,0 +1,4 @@ +Stats.builds++; +int[] _squares=new int[256]; +for(int i=0; i<256; i++) _squares[i]=i*i; +val square={u8 n -> _squares[n];}; diff --git a/chapter-sccp/README.ja.md b/chapter-sccp/README.ja.md index 71c78ee1..aac03cab 100644 --- a/chapter-sccp/README.ja.md +++ b/chapter-sccp/README.ja.md @@ -18,7 +18,7 @@ - ノードから型への対応(`Node._type`)を持っています。 - すでに伝達関数(`Node.compute`)を持っています。 -すべての `_type` を、⊥ ではなく ⊤ に初期化し、すべてのノードをワークリストに置きます。そこからノードを取り、伝達関数 `node.compute()` を実行します。結果が `node._type` と変われば更新し、その出力側の隣接ノードをワークリストへ入れます。アサーションとデバッグ補助をすべて取り除いた簡略版は次のとおりです。 +すべての `_type` を、⊥ ではなく ⊤ に初期化し、すべてのノードをワークリストに置きます。そこからノードを取り、伝達関数 `node.compute()` を実行します。結果が `node._type` と変われば更新し、その出力側の隣接ノードをワークリストへ入れます。依存関係の追跡とデバッグ用の検査の大部分を省いた簡略版は次のとおりです。 ```java while( (n = code._iter._work.pop()) != null ) { @@ -31,7 +31,7 @@ while( (n = code._iter._work.pop()) != null ) { } ``` -このアルゴリズムは必ず停止し、一般にはかなり速く、束と伝達関数の制限内での「最良の答え」である *不動点* に必ず到達します。得られる型はピープホール規則で見つけたものより単調に改善されます。その後、型が改善されたノードでピープホール規則を実行し、無関係なピープホール最適化が適用できるかを調べられます。たとえば新しく発見した定数が、計算を ConstantNode に置き換えます。 +単調な伝達関数と、範囲の拡大回数を制限する widening により、アルゴリズムは *不動点* で停止します。widening は範囲の精度を落とすことがあるため、束で表せる最も精密な答えとは限りません。得られる型は悲観的解析と同等以上の精度です。その後、型が改善されたノードでピープホール規則を実行できます。たとえば新しく発見した定数が、計算を ConstantNode に置き換えます。 以下は SSA の擬似コードです。`phi` はマージを意味し、Simple の構文ではありません。 @@ -46,7 +46,7 @@ return x1 SCCP がなければ、この小さなプログラムは `x` の計算を畳み込めません。見れば `x` は常に `1` とわかりますが、下から上へ進む悲観的手法は、x1 が BOT なので x2 が BOT、したがって x1 が BOT と判断します。行き詰まりを解くには楽観的手法が必要です。x1 が `1` と *仮定* すれば、x2 も `1` と発見できます。値の畳み込みは、ループ停止を証明したり、その条件と副作用を除去可能にしたりするものではありません。 -アルゴリズムは左から右へ進み、変化がなくなったら停止します。 +表はワークリストの処理順の一例を左から右に示します。変化がなくなったら停止します。 | 変数 | 初期 | x0=1 | x1=phi(1,T) | x2=2-x1 | x1=phi(1,1) | |---|----|----|-----------|-------|-----------| @@ -72,7 +72,7 @@ while( rand ) { return x1 ``` -再び左から右へ進みますが、今度はすべての経路が到達可能とは限りません。`if` の結果は Control 値の対で、経路が生きていれば `C`、そうでなければ `X` で示します。これは `x4 = phi(x2,x3)` の計算で使います。`PhiNode` は `if` の対応する制御入力 *も* 確認し、生きた経路だけをマージします。 +次の表も処理順の一例ですが、今度はすべての経路が到達可能とは限りません。`if` の結果は Control 値の対で、経路が生きていれば `C`、そうでなければ `X` で示します。これは `x4 = phi(x2,x3)` の計算で使います。`PhiNode` は `if` の対応する制御入力 *も* 確認し、生きた経路だけをマージします。 | 変数 | 初期 | x0=1 | x1=phi(1,T) | 1 == 1? | x2=2-x1 | x3=99 | x4=phi(C/1,X/99) | x1=phi(1,1) | |---|-----|----|-----------|---------|---------|---------|----------------|-----------| @@ -85,7 +85,7 @@ return x1 この種の条件付き定数伝播は、条件が真か偽と証明できると、最適化後のプログラムから検査と分岐を除去するので、非常に役立ちます。よくあるのが null 検査です。同じプログラムで、整数の代わりに null ポインタを使います。 -```java +```text List head = notNullList(); while( rand ) { if( head ) head = new List(head,payload); @@ -95,7 +95,7 @@ while( rand ) { `head` は非 null で始まるため、決して null にならず、`default_action` も起きません。ループは簡略化できます。 -```java +```text List head = notNullList(); while( rand ) { head = new List(head,payload); diff --git a/chapter-sccp/README.md b/chapter-sccp/README.md index 3b7599da..eec9a361 100644 --- a/chapter-sccp/README.md +++ b/chapter-sccp/README.md @@ -16,8 +16,9 @@ Chapter 24. The numbered snapshot remains a comparison baseline. ## Sparse Conditional Constant Propagation [SCCP](https://en.wikipedia.org/wiki/Sparse_conditional_constant_propagation) -is the top-down version of the same peephole/constant-folding work we have -been doing all along. The rules for the algorithm are well known from the +is the optimistic version of the same peephole/constant-folding work we have +been doing all along. Types start at the top of the lattice and fall as we +discover reachable values. The rules for the algorithm are well known from the literature and commonly taught at the college level; [example slides](https://www.cse.psu.edu/~gxt29/teaching/cse522s23/slides/08monotoneFramework.pdf). @@ -30,7 +31,7 @@ We initialize all `_type` fields to ⊤ (as opposed to ⊥), and place all Nodes a worklist. We pull a Node from the worklist, run the transfer function `node.compute()`, and if the result changes from `node._type` we update `node._type` and put its output neighbors on the worklist. A simplified -version with all asserts and debug helpers removed: +version, omitting dependency tracking and most debug checks: ```java while( (n = code._iter._work.pop()) != null ) { @@ -43,10 +44,11 @@ while( (n = code._iter._work.pop()) != null ) { } ``` -This algorithm always terminates (and generally fairly quickly) and always -reaches a *fixed point*, as "best answer" subject to the limits of our lattice -and transfer functions. The resulting types are monotonically better than -those found by the peephole rules; we can then run the peephole rules on nodes +With monotone transfer functions and bounded range widening, this algorithm +terminates at a *fixed point*. Widening can lose range precision, so this need +not be the sharpest answer the lattice can express. The resulting types are +at least as precise as those found by pessimistic analysis. We can then run +the peephole rules on nodes with improved types, to see if unrelated peepholes can fire - such as newly discovered constants replacing computations with ConstantNodes. @@ -70,8 +72,8 @@ also a `1`. Folding the value does not prove the loop terminates or make its condition and side effects removable. -The algorithm progresses from left to right and stops when no more changes -happen: +The table shows one possible worklist order, from left to right. The solve +stops when no more changes happen: |Var|init|x0=1|x1=phi(1,T)|x2=2-x1|x1=phi(1,1)| |---|----|----|-----------|-------|-----------| @@ -101,7 +103,7 @@ while( rand ) { return x1 ``` -Again the algorithm progresses from left to right, although this time not all +Again the table shows one possible order, although this time not all paths are reachable. The `if` result is a pair of Control values, shown as either `C` if the path is alive or `X` if not. This is used when we compute `x4 = phi(x2,x3)`; the `PhiNode` ALSO checks the corresponding control input @@ -120,9 +122,9 @@ from the `if` and only merges alive paths. This kind of conditional constant propagation really helps if a condition can be proven true or false, as this removes a test and branch from the optimized program. A common way this happens is with null tests; here is the same -program above, but using null pointers instead of integers: +idea as above, but using null pointers instead of integers (in pseudocode): -```java +```text List head = notNullList(); while( rand ) { if( head ) head = new List(head,payload); @@ -134,7 +136,7 @@ Here since `head` starts not-null, it can never be null and the `default_action` can never happen, and the loop can simplify: -```java +```text List head = notNullList(); while( rand ) { head = new List(head,payload); diff --git a/chapter19/src/test/java/com/seaofnodes/simple/Chapter19Test.java b/chapter19/src/test/java/com/seaofnodes/simple/Chapter19Test.java index 38e9923d..2ef362d6 100644 --- a/chapter19/src/test/java/com/seaofnodes/simple/Chapter19Test.java +++ b/chapter19/src/test/java/com/seaofnodes/simple/Chapter19Test.java @@ -47,7 +47,7 @@ return 0; """ struct String { u8[] !cs; - int _hashCode; + int hashCode; }; val equals = { !String !self, !String s -> @@ -60,12 +60,12 @@ val equals = { !String !self, !String s -> }; val hashCode = { !String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); + self.hashCode + ? self.hashCode + : (self.hashCode = hashCodeString(self)); }; -val _hashCodeString = { !String self -> +val hashCodeString = { !String self -> int hash=0; if( self.cs ) { for( int i=0; i< self.cs#; i++ ) diff --git a/chapter25/README.ja.md b/chapter25/README.ja.md index b2ef41fb..4feaee93 100644 --- a/chapter25/README.ja.md +++ b/chapter25/README.ja.md @@ -9,7 +9,7 @@ この章では Simple のソースファイルを再利用可能なオブジェクトファイルへコンパイルし、その型と理想 IR を後のコンパイルに読み込み、別に構築したシステムライブラリとネイティブプログラムをリンクします。また、不完全な情報を扱うため、パーサー、型、メモリ SSA を大幅に再設計します。 -これらが現在のスナップショットの内容です。独立した正しさの修正を先に以前の章へ移します。大きな SSA 再設計を以前へ移すことや、この章の分割は延期します。[バックポート待ち一覧](../docs/chapter-backports.md) を参照してください。 +この番号付きスナップショットは比較用に残しています。章の分割は進行中で、型未確定時の SSA 構築、オブジェクトの構築、エスケープ解析、モジュールを名前付きの章に分けました。[分割計画](../docs/chapter-split.md) と [バックポート待ち一覧](../docs/chapter-backports.md) を参照してください。 第21〜24章のメモリ処理はここにも引き継ぎます。Load 検索は読み取りをループ外へ移す前にマージの全枝を証明し、Phi の因数分解はメモリ破壊を確認し、スケジューリングはメモリ集合体を通じてエイリアスをたどります。検索は `EscapeNode` から非公開または公開メモリへ進み、未解決の Store エイリアスを待ちます。配列長の読み取りには要素の境界検査がなく、配列要素の読み取りは制御を保持します。整数値の Phi は、既存の型付き変換機構で、ループの拡大を通じてもロード幅を保持します。New は引き続き非公開のコンストラクタメモリを生成し、公開は Escape が扱います。以前の章の割り当て入力設計は、これを置き換えません。 @@ -23,7 +23,7 @@ 各単位は Start/Stop ノードを持ちます。関数がコンパイル単位に所有されることと、未知の外部呼び出し側を表す辺とは別です。出力のためにコードを保持することが、任意の呼び出し側から到達できることを意味してはいけません。 -ドライバは、プロジェクトルートの `--root`、出力の `-o`、外部 Simple オブジェクト検索パスの `-L` を受け付けます。検索パスにはオブジェクトファイルやディレクトリを指定できます。コンパイラはネイティブリンク用の外部 C シンボルも解決します。`_` で始まる名前は非公開性を制御し、公開到達性は外側の名前にも依存します。 +ドライバは入力パスからプロジェクトルートを推定し、出力の `-o` と外部 Simple オブジェクト検索パスの `-L` を受け付けます。検索パスにはオブジェクトファイルやディレクトリを指定できます。ヘルプには `--root` もありますが、この番号付きスナップショットでは未実装です。名前付きのモジュール章では使用できます。コンパイラはネイティブリンク用の外部 C シンボルも解決します。`_` で始まる名前と外側の名前は、未知の呼び出し元に公開するインターフェースを決めます。 明示的な数値型の C 宣言は、可変のネイティブ記憶領域へ直接束縛します。 @@ -98,26 +98,13 @@ return new Box(new i64[1]); 第21章の隣接コピー転送も引き継ぎます。次の命令だけで使うコピーは、オペランドがコピー元レジスタを受け入れ、命令の結果に結び付けられていなければ除去します。 -以下は、必須の `cs` フィールドに明示的なコンストラクタを与えて保存済み String ソース3件を修正した後の、完全な2026-10-02再実行です。過去の212項目すべてが成功します。これはフィクスチャ調整と最近のコンパイラ修正後の新たな基準値を確立します。以前の監査との割り当て器だけの比較ではありません。 +現在の比較は、固定したテスト群を行、コンパイラの章を列にした +[英語版の測定表](README.md#regalloc-improvements-live-range-area-and-split-cost) を参照してください。 +各セルは ARM/RISC-V の実行命令数、そのうち割り当て器が生成したコピーなどの命令数、 +x86 のループ重み付き移動数を示します。これらは異なる指標です。 +対象、入力、除外項目は [測定方法](../docs/regalloc-spills.md) にあります。 +古い測定群の集計値と混ぜて比較しないでください。 -`make spill-stats` を実行してください。各行は **第25章のコンパイラ** を使います。最初の5行は以前の212のプログラム/対象項目を、この章の構文とライブラリ構成に適応して保持しています。[フィクスチャノート](src/test/java/com/seaofnodes/simple/spill/README.ja.md) にその調整と測定の制限を記載しています。最適化器のシードは123です。最後の行には、既存シードで記録した `Chapter25Test` の13割り当てと、ドライバのシード456での新規システムライブラリコンパイルを含みます。`sys.o` を再利用して、この大きな処理対象を測定から消してはいけません。この結果は Windows x86-64 のものです。以前の群には RISC-V/ARM SystemV と x86 SystemV も含みます。報告器は対象ごとの合計も表示します。 - -| プログラム群 | コンパイル数 | 分割/移動数 | ループ重み付き数 | -|---|---:|---:|---:| -| 第20章 | 39 | 375 | 606 | -| 第21章 | 52 | 479 | 1,172 | -| 第22章 | 24 | 103 | 103 | -| 第23章 | 30 | 129 | 353 | -| 第24章 | 67 | 696 | 1,410 | -| 第25章 | 14 | 786 | 1,815 | -| **合計** | **226** | **2,568** | **5,459** | - -`_spills` はレジスタ間移動を含む残存 SplitNode を数え、`_spillScaled` はループ重みを適用します。どちらもメモリアクセスだけを測るものではありません。古い群はレジスタ合法性確認を伴う割り当ての再実行で、ネイティブハーネスの再実行ではありません。`Chapter25Test` の23テストすべてが、ネイティブ/結果確認を含め、スピル期待値の失敗なしに成功します。継承したエミュレータテストを含む章全体には `make -j 4 tests` を使ってください。 - -### 測定についてのコメント - -以前の割り当て器監査では、一様な改善は見つかりませんでした。そのコンパイラのほかの部分を変えず、第24章の順位付けに置き換えると、221のクライアントコンパイルで5移動減りました(2,211/4,724に対し2,206/4,719)。しかしシステムライブラリを8ラウンド以内に割り当てられませんでした。したがって不完全な比較であり、有効なスイート全体の低い合計ではありません。既存の面積/コスト順位付けを保持し、ラウンド上限と正しさの確認も変えません。その監査でシステムライブラリ自体は386移動/855重み付き移動で、現在の再実行は373/828です。 - -複数使用を持つすべての複製可能定数を優先する試行は、それらのクライアントで以前の順位付けより1移動減りましたが、やはり新規ライブラリビルドに失敗し、却下されました。これが、集計統計と完全なコンパイル・実行確認の両方が重要な理由です。有利な小計だけでは変更を正当化できません。 - -その以前の監査では、過去の群の合計が第24章の1,332/2,956から、第25章の1,814/3,753へ増えました。第25章はクラス初期化子、モジュール所有権、異なる脱出解析、構文/ライブラリの調整を追加します。これらは異なるマシングラフなので、増加を割り当て器の品質だけに帰することはできません。章の表から結論を出す前に、1つのコンパイラと固定した入力集合でヒューリスティックを比較してください。 +クラス初期化、インライン化、エスケープ解析、ライブラリの変更もグラフを変えます。 +章をまたぐ差を割り当て器だけの品質差と解釈せず、同じ入力とコンパイラで +ヒューリスティックを比較してください。 diff --git a/chapter25/README.md b/chapter25/README.md index bbe0c50e..d6ab5666 100644 --- a/chapter25/README.md +++ b/chapter25/README.md @@ -27,10 +27,13 @@ Each unit has Start/Stop nodes. A function's ownership by a compilation unit is distinct from an edge representing unknown external callers: retaining code for emission must not imply that arbitrary callers can reach it. -The driver accepts `--root` for the project root, `-o` for output, and `-L` for -external Simple object search paths, including an object file or a directory. +The driver infers the project root from the input path and accepts `-o` for +output and `-L` for external Simple object search paths, including an object +file or a directory. Its help advertises `--root`, but this numbered snapshot +does not yet parse that option; the named modules snapshot does. The compiler also resolves external C symbols for native linking. Names beginning -with `_` control privacy; public reachability depends on enclosing names too. +with `_` control the public interface; enclosing names also affect which +entries are exposed to unknown callers. Explicit numeric C declarations bind directly to mutable native storage: @@ -255,10 +258,10 @@ metric; see the [measurement details](../docs/regalloc-spills.md) for coverage. <!-- spill-matrix:end --> -Chapter25 introduces several features, which when run on the older tests +Chapter 25 introduces several features, which when run on the older tests generate a lot more code, which in turn generates more spills. Class initialization, inlining, escape analysis, and library adaptations all change the graphs. To help with this, we adjust the inlining -heursitics to try and get all the boilerplate to inline and optimize. +heuristics to try to inline and optimize the boilerplate. diff --git a/docs/chapter-backports.md b/docs/chapter-backports.md index d44a66d3..a7d3cfca 100644 --- a/docs/chapter-backports.md +++ b/docs/chapter-backports.md @@ -11,7 +11,8 @@ boundaries; revisit construction/escape after he can run both snapshots. Library construction and optimizer tuning move to appendices. The first named snapshots are `chapter-sccp`, `chapter-interprocedural-sccp`, -`chapter-incomplete-types`, `chapter-construction`, and `chapter-escape`. +`chapter-incomplete-types`, `chapter-construction`, `chapter-escape`, and +`chapter-modules`. Local SCCP keeps unknown callers and the call result summaries from pessimistic analysis; the next chapter discovers calls and widens recursive arguments and returns. Numbered Chapters 24 and 25 remain @@ -22,17 +23,168 @@ symbolic fields, and unbounded Load/Phi nodes. Construction now has hidden initializer/allocator calls, local field values and Phis, exit Stores, and explicit publication. Private memory carries fields between helpers. Escape now discovers exposed aliases/functions through the Start/Stop cycle. Review -the construction/escape border before extracting modules and lazy class -initialization. Chained comparisons still need their move to Chapter 23; -allocator tuning and native library material still need their appendices. +the construction/escape border as needed. Modules/lazy initialization now has a +runnable snapshot; separate compilation is next. Chained comparisons still need +their move to Chapter 23; allocator tuning and native library material still +need their appendices. Root Make includes named snapshots in checks. Linear-history publication keeps the numbered sequence until the complete replacement sequence is ready; switch its ordering and index links when retiring the comparison baselines. +## Four README intentions to review + +Keep these four issues visible while reviewing the named chapters. Decisions +may change the construction/escape boundary or later chapter splits. + +1. **Construction (active):** final fields should be local SSA values during + construction, including repeated assignments and loops, with field Stores + at helper exits. Silent loss of writes is a bug; reject unsupported cases. + Also decide whether to allow helpers after inlining and defer completion + checks until SCCP. Those are separate from local field updates. + The local-value implementation now supports final data fields in construction, + escape, and modules, including constant defaults and loop Phis. Shared + function declarations remain fixed. Validate this frontier before adjusting + the broader constructor rules. +2. **Privacy:** underscore names select the public interface, but sibling access + to private fields is not yet rejected. Confirm and enforce the intended + source access rules. The proposed chapter boundary and library changes are + recorded below; Cliff reviews that proposal before implementation. +3. **Class initialization:** review the corrected rule: A may start, initialize + B, then resume. Passive references may cycle; active dependencies may not. + Parents finish before children start. The README now matches this behavior; + further semantic changes need an explicit decision. +4. **Escape summaries and ELF visibility:** analysis tracks unknown callers, + while the current ELF writer exports retained named functions. Decide which + symbols should be public and how that boundary relates to separate compilation. + +### Privacy proposal for review + +Introduce enforced underscore privacy in **Modules and Lazy Class +Initialization** (`chapter-modules`). This is a visible language change: +earlier snapshots allow underscore field access; from Modules onward, access +must obey the declaring class's boundary. The teaching outline already places +privacy here, alongside class names, ownership, and the public interface. +The earlier escape snapshot does not filter fields by underscore spelling. +Its private construction memory is a different concept. + +Review follow-up: preserve the distinction between export filtering and access +checks. Before the 2026-10-24 cleanup, the first underscore helper in the +numbered snapshots' tests was Chapter 19's `Chapter19Test.testString`, +`_hashCodeString`. Renaming it to +`hashCodeString` produces the identical LocalSched graph there: single-use +inlining removes it regardless of spelling. In Chapter 25's copy of that test, +the same rename retains an extra function Return. Underscore export filtering +currently starts in Chapter 25 (and its Modules extraction), not Chapter 19. +Old test class names inside newer snapshots do not identify when their current +underscore behavior was introduced. Any access-check change must preserve the +existing ability to hide helpers and classes from the public interface. +Chapter 19's example now uses `hashCodeString` and field `hashCode`; all 42 +Chapter19Test tests pass with the existing expected graph unchanged. + +Proposed rules: + +- A field or method beginning with `_` is accessible only from code in its + declaring class, including its constructors and method bodies. Enclosing, + nested, and sibling classes do not gain access to that field. A same-class + method may access another instance's private fields; the receiver need not + be exactly `self`. +- A private class name is available in its declaring scope and within the + class itself. For example, `App` may name its child `_Cache`, but that does + not grant `App` access to `_Cache._data`. Code outside that declaring scope + cannot bypass the restriction with a dotted path. Private ancestors also + keep their descendants out of the public interface. Check both type names + and class-value references, and distinguish naming a private class from + using a public value returned by a factory. +- Reads, writes, updates, and method references follow the same rule. + Preserve the source owner and location when receiver types are unresolved; + inlining must not change who has access. An unused result or a folded Load + must not erase an illegal source access. +- Native declarations obey the same source rules. A public wrapper or alias + may deliberately expose a private native binding; no exception for `sys`. + +Source audit: + +- `sys/io.smp` calls `libc._exit` twice. Keep the native `_exit` declaration + private in `sys/libc.smp`, add `val exit = { i32 status -> _exit(status); };`, + and use `libc.exit` in IO. This preserves the native symbol and behavior. +- Bitset's `_bits` and `_grow`, including `_grow(bit)._bits`, are used within + the declaring struct and remain legal. Array helpers call their own `_grow`. + These are the other underscore names in the bundled module library. +- Several retained String spill fixtures use free functions outside `_String` + to access `_hashCode`. They will need an explicit adaptation in this snapshot + (for example, making that fixture field public), with any changed allocation + expectations measured. Do not silently grant file-wide access to pass them. +- The numbered Chapter 25 library has the same `_exit` accesses, and its + `docs/examples/FileIO.smp` also calls `sys.libc._exit`. + +After approval, implement and test in Modules, update its English/Japanese +READMEs with the explicit language boundary, and run the full module suite +including bundled `sys` and native execution. Cover allowed same-class access, +rejected cross-class access, private class lookup, and forward receiver types. +Port the rule and library adaptations to the numbered Chapter 25 comparison +snapshot, with import tests preserving the original source owner. Carry it +into the forthcoming separate-compilation chapter. Do not backport the new +language restriction into Chapters 10-24 or the named pre-module snapshots. + +## Proven Chapter 25 follow-ups from the module extraction + +Three failures reproduce in the numbered Chapter 25 baseline. The fixes are in +`chapter-modules`, but have not been copied back into that comparison snapshot: + +- A private recursive callback passed to native code retains `base == 2` from + known calls instead of restoring its declared `i64` bound. See + `BoundaryTest.testNativeExposure`. Carry direct Call exposure to StopCU even + on nonreturning paths, and restore Start when a function already has known + callers. `Opto.linkStart` must test unknown callers, not `nIns() < 2`. +- The lazy-table example crashes in `StoreNode.useAlias` when a symbolic class + Store has an EscapeNode user from a later allocation. Like + `BulkMemPhiNode.outputAlias`, use the publication field's alias when the Store + is that Escape's public-memory input. See `BoundaryTest.testLazyTables` and + `chapter-modules/src/test/smp/lazy`. The numbered baseline throws `Untested`. +- Final constructor fields lose writes when their declaration value is constant: + `val x=1; new C={->x=1; x=2;};` still reads back 1. Keep constructor field + declarations broad enough for later values, build loop Phis for final fields, + and preserve the distinction from final local bindings. See + `FinalConstructionTest` in the named snapshots. Recheck serialized declarations + and imported constructors when carrying this to the numbered baseline. + +Disposable reproducer: `build/SplitProbe.java`, compiled against Chapter 25; +arguments `native` and `array` exercise these failures. Port with the baseline's +import tests enabled, since the module snapshot intentionally lacks imports. + +Constructor local SSA, deferred constructor parsing and converted private +summaries also need to survive the final separate-compilation extraction. +These carry the already-reviewed construction design forward; they are not +independent changes for pre-construction chapters. + ## Investigate next Establish an earlier-chapter failure before changing compiler behavior. +### Constructor loop with break/continue: worklist assertion + +This fails in construction and in the unchanged escape compiler before the +final-field fix, even with a mutable field: + +```simple +struct C { + int x=0; + new C={int n -> + for(int i=0;i<n;i++) { + if(i==1) continue; + if(i==3) break; + x+=i+1; + } + }; +}; +return new C(arg).x; +``` + +`IterPeeps.progressOnList` asserts during Iter with seed 123. The expected value +for arg 3 is 4. This is independent of final-field mutability; retain the +reproducer while reviewing constructor coverage. Scratch probe: +`build/readme-review/mutable_break.smp`. + ### SCCP function revival: Chapter 24 `Opto.linkCG` uses `code.link(fidx)`, which clears a linker entry when its @@ -102,12 +254,13 @@ the construction chapter. They are not independent fixes to copy into earlier snapshots. Serialization, global identity remapping, and imported escape summaries belong -with separate compilation. Escape analysis gets its own earlier chapter after -construction; its extraction boundary is still to be established. +with separate compilation. Escape analysis now has its own earlier snapshot after +construction; keep its boundary open to review. ## Work that stays in Chapter 25 -Class-cycle checking and CLInit runtime lowering stay in Chapter 25. The +Class-cycle checking and CLInit runtime lowering also appear in the new +modules snapshot; they do not belong in the pre-module chapters. The baseline [chapter README](../chapter25/README.md#lazy-class-initialization) explains the implementation; future work lives in the [roadmap](../chapter25/ROADMAP.md#class-initialization-and-compiler-follow-ups). diff --git a/docs/chapter-split.md b/docs/chapter-split.md index 2b32c83d..7f257d43 100644 --- a/docs/chapter-split.md +++ b/docs/chapter-split.md @@ -117,8 +117,9 @@ The first runnable extractions are [SCCP Across Functions](../chapter-interprocedural-sccp/README.md), followed by [SSA Before Types Are Known](../chapter-incomplete-types/README.md), [Construction and Private Memory](../chapter-construction/README.md) and -[Escape Analysis and Unknown Callers](../chapter-escape/README.md). -The other two snapshots remain to be extracted. Numbered Chapters 24 and 25 +[Escape Analysis and Unknown Callers](../chapter-escape/README.md), and +[Modules and Lazy Class Initialization](../chapter-modules/README.md). +Separate compilation remains to be extracted. Numbered Chapters 24 and 25 remain comparison baselines during this work. Choose final numbers after trying the implementation boundaries above. @@ -153,3 +154,29 @@ Construction still keeps local field values and rejects early exposure of self. Both runnable snapshots are available for Cliff's promised review/play session. This extraction does not choose broader rules for partially initialized objects. No modules, class singletons, serialized IR or identity remapping are included. + +## Modules snapshot ready for review + +`chapter-modules` compiles all discovered source files together. It adds class +singletons, namespaces and privacy, parent initialization, CLInit memory effects, +cycle diagnostics, runtime lowering, and shared native class storage. Source +discovery reads parents as well as referenced children. The bundled modular sys +is supplied as source; building reusable libraries remains appendix material. + +There is no Serialize phase, object reader, GlobalBits remapping, or imported +IR. LocalIds assigns dense IDs within the invocation. FinishCalls detaches +analysis call edges after loop analysis, before scheduling or instruction +selection. ELF emits all discovered live bodies in the selected entry object. + +The preceding construction representation remains: field values and loop Phis +stay local, with Stores at helper exits. Constructor reads may see an unfinished +object, but may never read an uninitialized field. The module snapshot checks +this separately from completion and publication. Escape analysis still restores +unknown callers when a private callback reaches native code, including on paths +with no Return. + +The README's lazy lookup-table example is executable. Existing tests also cover +source/type cycles, rejected initialization cycles, parent order, native cold/hot +calls, shared storage, and ARM/RISC-V encoding. The construction/escape border +remains open to Cliff's review; this extraction does not settle broader rules for +publishing unfinished objects. @@ -50,6 +50,7 @@ <module>chapter-incomplete-types</module> <module>chapter-construction</module> <module>chapter-escape</module> + <module>chapter-modules</module> </modules> <properties> diff --git a/skills/chapter25-codex-notes.md b/skills/chapter25-codex-notes.md index 7fe2724c..c86dcb8e 100644 --- a/skills/chapter25-codex-notes.md +++ b/skills/chapter25-codex-notes.md @@ -17,7 +17,7 @@ to review/play with construction versus escape after the snapshots run. Keep that border open to refinement. Revisit 17a/17b when choosing final names or numbers. The first extracted snapshots are chapter-sccp, chapter-interprocedural-sccp, chapter-incomplete-types, chapter-construction, -and chapter-escape; numbered 24/25 +chapter-escape, and chapter-modules; numbered 24/25 remain comparison baselines. For shared viewer work, read [the graph viewer notes](graph-viewer-codex-notes.md). @@ -68,6 +68,18 @@ For shared viewer work, read [the graph viewer notes](graph-viewer-codex-notes.m ## Mutability and chapter boundaries +- The four README intention questions are kept in + [the review agenda](../docs/chapter-backports.md#four-readme-intentions-to-review). + Final data fields are writable constructor locals, including in loops; final + local bindings and shared function declarations stay fixed. Do not classify + a constant instance default as a shared field or skip its loop Phi merely + because the published field is final. Class constants keep their static image. + +- The modules snapshot is source-only: LocalIds reserves class slots within one + invocation; ParseAll reads discovered parents and children; FinishCalls removes + analysis call edges before scheduling/selection. No Serialize, ElfReader, + GlobalBits or imported type reconstruction. Keep the constructor and native + exposure cases in BoundaryTest when extracting separate compilation. - A symbolic Store with alias 1 is a barrier to Load forwarding: until its field resolves it may write the queried alias. Skipping it can turn a byte compound assignment at 255 into a stale read of 255 instead of 2. Both @@ -79,6 +91,9 @@ For shared viewer work, read [the graph viewer notes](graph-viewer-codex-notes.m and keep unwritten fields visible to completion checks. Constructor bodies are parsed after the field list closes, so later fields also have local slots. The discarded InitStore/InitPhi design must not be brought into the EA split. + Constructor reads may see an unfinished object, but never an uninitialized + field. Exact self aliases must read the current local value, including inside + loops, rather than loading the allocation before exit Stores have run. Construction privacy follows language rules; global escape discovery is a separate topic, with no required teaching order between them. |
