summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-10-28 22:57:30 -0700
committerCliff Click <cliffc@acm.org>2026-10-28 22:57:30 -0700
commitf7fcac0ac9ebfc11549272fa8c166c26e5418e3f (patch)
tree14ae65327062b3e633eb1bf9ff686558047c4fe6
parent3df967056bf48bd9f87900f3a1ad9af2aedce948 (diff)
downloadSimple-f7fcac0ac9ebfc11549272fa8c166c26e5418e3f.tar.gz
Simple-f7fcac0ac9ebfc11549272fa8c166c26e5418e3f.zip
A more incremental StopCUmain
Incremental summary of escape data in a single node, instead of repeated in StopCU over and over
-rw-r--r--chapter28b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java5
-rw-r--r--chapter28b/src/main/java/com/seaofnodes/simple/codegen/Iter.java3
-rw-r--r--chapter28b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java2
-rw-r--r--chapter28b/src/main/java/com/seaofnodes/simple/node/CallNode.java21
-rw-r--r--chapter28b/src/main/java/com/seaofnodes/simple/node/GlobalEscapeNode.java40
-rw-r--r--chapter28b/src/main/java/com/seaofnodes/simple/node/Node.java5
-rw-r--r--chapter28b/src/main/java/com/seaofnodes/simple/node/StopCUNode.java51
-rw-r--r--chapter28b/src/main/java/com/seaofnodes/simple/node/SummaryNode.java1
-rw-r--r--chapter28b/src/main/java/com/seaofnodes/simple/type/TypeMem.java8
-rw-r--r--chapter28b/src/test/java/com/seaofnodes/simple/Chapter20Test.java2
-rw-r--r--chapter28b/src/test/java/com/seaofnodes/simple/Chapter21Test.java2
-rw-r--r--chapter28b/src/test/java/com/seaofnodes/simple/TypeTest.java17
-rw-r--r--docs/roadmap.ja.md2
-rw-r--r--docs/roadmap.md7
14 files changed, 120 insertions, 46 deletions
diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
index 2852e23d..81d99190 100644
--- a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
+++ b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
@@ -395,6 +395,9 @@ public class CodeGen {
// Start and Stop; end points of the generated IR
public StartNode _start;
public StopNode _stop;
+ // Iter creates this shared analysis result. Start's unused backedge slot
+ // roots it until Unlink; serialized Stops retain its escape facts instead.
+ public GlobalEscapeNode _globalEscapes;
// Global Value Numbering. Hash over opcode and inputs; hits in this table
// are structurally equal.
@@ -427,6 +430,7 @@ public class CodeGen {
public void link(FunNode fun) {
int fidx = fun.sig().fidx();
_linker.setX(fidx,fun);
+ add(_globalEscapes); // A new or replacement summary may expose escapes.
}
// "Linker" mapping from constant TypeFunPtrs to heads of function. These
@@ -627,6 +631,7 @@ public class CodeGen {
}
private void unlink(boolean removeImports) {
phase(Phase.Unlink, _phase==Phase.LoopTree, () -> {
+ _start.setDef(2,null); // Releases the compiler-only escape analysis.
if( removeImports ) Unlink.run(this,true);
else Unlink.forEval(this,_start,_stop);
});
diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Iter.java b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Iter.java
index 1ce5b051..4085ebff 100644
--- a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Iter.java
+++ b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Iter.java
@@ -8,6 +8,9 @@ import com.seaofnodes.simple.util.Ary;
// reuse the worklist and types produced by the preceding phase.
public abstract class Iter {
static void run(CodeGen code) {
+ assert code._globalEscapes==null;
+ code._globalEscapes = new GlobalEscapeNode();
+ code._start.setDef(2,code._globalEscapes);
// The phase shift closes all parser-in-progress states and freezes
// unknown external inputs. Imported nodes carry post-Opto escape
// precision, but Iter is a fresh pessimistic solve: memory escape
diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
index 32027cb5..98350a95 100644
--- a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
+++ b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
@@ -270,6 +270,8 @@ public class Serialize {
Integer idx = def==null ? Integer.valueOf(0) : anodes.get(def);
// Imported function constants retain their symbol, not a body.
if( idx==null && n instanceof FunPtrNode && j==1 ) idx=0;
+ // Shared escape analysis belongs to this compilation only.
+ if( def instanceof GlobalEscapeNode ) idx=0;
assert idx != null : "Missing serialized input for "+n+" input "+j+" def "+def;
baos.packed2(idx);
}
diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/node/CallNode.java b/chapter28b/src/main/java/com/seaofnodes/simple/node/CallNode.java
index 12c7f1e3..29d4f6aa 100644
--- a/chapter28b/src/main/java/com/seaofnodes/simple/node/CallNode.java
+++ b/chapter28b/src/main/java/com/seaofnodes/simple/node/CallNode.java
@@ -23,7 +23,12 @@ 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(Parser.Lexer loc, Node... nodes) {
+ super(nodes);
+ _loc = loc;
+ CodeGen.CODE._calls.add(this);
+ CodeGen.CODE.add(CodeGen.CODE._globalEscapes);
+ }
public CallNode(CallNode call) { super(call); _loc = call._loc; }
@Override public Tag serialTag() { return Tag.Call; }
public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) {
@@ -97,10 +102,16 @@ public class CallNode extends CFGNode {
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);
+ if( _type.isHigh() ) return TypeMem.START;
+ // A broad scalar target can refine to a function pointer during Iter.
+ // It cannot yet promise that the call exposes nothing.
+ if( _type!=Type.CONTROL ) return TypeMem.BOT;
+ if( !(fptr()._type instanceof TypeFunPtr) )
+ return fptr()._type.isHigh() ? TypeMem.START : TypeMem.BOT;
+ if( !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);
}
diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/node/GlobalEscapeNode.java b/chapter28b/src/main/java/com/seaofnodes/simple/node/GlobalEscapeNode.java
new file mode 100644
index 00000000..dab48530
--- /dev/null
+++ b/chapter28b/src/main/java/com/seaofnodes/simple/node/GlobalEscapeNode.java
@@ -0,0 +1,40 @@
+package com.seaofnodes.simple.node;
+
+import com.seaofnodes.print.ExprPrinter;
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.*;
+
+// Escapes from calls and unloaded bodies, shared by all compilation units.
+// These include paths which never Return. Iter creates this after parsing;
+// the outer Start holds it until Unlink. Saved objects contain the resulting
+// StopCU types instead. Dependencies wake this node when contributors change;
+// Stops need waking only when the combined result changes.
+public class GlobalEscapeNode extends Node {
+ public GlobalEscapeNode() { super(new Node[0]); _type = TypeMem.BOT; }
+ @Override public String label() { return "GlobalEscape"; }
+ @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p(label()); }
+ @Override public Node idealize() { return null; }
+
+ @Override public TypeMem compute() {
+ TypeMem xmem = StopCUNode.priorEscapes(_type);
+ for( CallNode call : CodeGen.CODE._calls )
+ if( !call.isDead() ) {
+ xmem = StopCUNode.publicBottom(xmem.escapes(call.exposed(this)),xmem);
+ // FULL memory normalizes to public bits. Preserve precise
+ // private arguments independently of that normalization.
+ if( call._type==Type.CONTROL && call.exposesArguments(this) )
+ for( int i = 0; i<call.nargs(); i++ )
+ xmem = StopCUNode.publicBottom(xmem.escapes(addDepForwards(call.arg(i+2))._type),xmem);
+ }
+ for( FunNode fun : CodeGen.CODE._linker )
+ if( fun!=null && fun.summary()!=null && !fun.summary().isDead() ) {
+ xmem = StopCUNode.publicBottom(xmem.escapes(fun.summary().exposed(this)),xmem);
+ // A reachable unloaded body can call these targets, including
+ // private initializers. Its arguments are unavailable here.
+ if( fun._type==Type.CONTROL )
+ for( int fidx : fun.summary()._classDependencyFIDXs )
+ if( fidx!=0 ) xmem = xmem.makeFrom(XInt.meet(xmem._escFs,XInt.make(fidx)),xmem._escAs);
+ }
+ return xmem;
+ }
+}
diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/node/Node.java b/chapter28b/src/main/java/com/seaofnodes/simple/node/Node.java
index 0c704a7d..515b29f4 100644
--- a/chapter28b/src/main/java/com/seaofnodes/simple/node/Node.java
+++ b/chapter28b/src/main/java/com/seaofnodes/simple/node/Node.java
@@ -717,7 +717,10 @@ public abstract class Node extends BaseNode<Node> implements Cloneable {
n._outputs = new Ary<>(Node.class);
n._deps = null;
n._hash = 0;
- if( n instanceof CallNode call ) CODE._calls.add(call);
+ if( n instanceof CallNode call ) {
+ CODE._calls.add(call);
+ CODE.add(CODE._globalEscapes);
+ }
return n;
}
diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/node/StopCUNode.java b/chapter28b/src/main/java/com/seaofnodes/simple/node/StopCUNode.java
index 61970aa0..a6116e93 100644
--- a/chapter28b/src/main/java/com/seaofnodes/simple/node/StopCUNode.java
+++ b/chapter28b/src/main/java/com/seaofnodes/simple/node/StopCUNode.java
@@ -35,16 +35,7 @@ public class StopCUNode extends StopNode {
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));
+ TypeMem xmem = priorEscapes(_type instanceof TypeTuple old ? old.mem() : _type);
for( Node def : _inputs ) {
ReturnNode ret = (ReturnNode)def;
@@ -91,27 +82,11 @@ public class StopCUNode extends StopNode {
xmem = (TypeMem)xmem.meet(publicBottom(tret.mem(),xmem));
}
}
- // Native and unknown-caller retention includes 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)),xmem);
- // FULL memory normalizes to public bits. Preserve precise
- // private arguments independently of that normalization.
- if( call._type==Type.CONTROL && call.exposesArguments(this) )
- for( int i = 0; i<call.nargs(); i++ )
- xmem = publicBottom(xmem.escapes(addDepForwards(call.arg(i+2))._type),xmem);
- }
- for( FunNode fun : CodeGen.CODE._linker )
- if( fun!=null && fun.summary()!=null && !fun.summary().isDead() ) {
- xmem = publicBottom(xmem.escapes(fun.summary().exposed(this)),xmem);
- // A reachable unloaded body can call these targets, including
- // private initializers. Its arguments are unavailable here.
- // Preserve these precise dependencies after normalizing FULL.
- if( fun._type==Type.CONTROL )
- for( int fidx : fun.summary()._classDependencyFIDXs )
- if( fidx!=0 ) xmem = xmem.makeFrom(XInt.meet(xmem._escFs,XInt.make(fidx)),xmem._escAs);
- }
+ // All units share escapes from calls and unloaded bodies, including
+ // paths with no Return. Revisit only when that shared result changes.
+ GlobalEscapeNode global = CodeGen.CODE._globalEscapes;
+ if( global!=null && !global.isDead() )
+ xmem = publicBottom(xmem.escapes(addDepForwards(global)._type),xmem);
assert xmem._alias==1 && !xmem._one;
return TypeTuple.make(Type.CONTROL,xmem,Type.BOTTOM);
}
@@ -119,7 +94,7 @@ public class StopCUNode extends StopNode {
// 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, TypeMem prior) {
+ static 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.
@@ -135,6 +110,18 @@ public class StopCUNode extends StopNode {
return escFs==mem._escFs && escAs==mem._escAs ? mem : mem.makeFrom(escFs,escAs);
}
+ // In Opto, a precise escape set can widen to FULL. Public-only
+ // normalization must retain private bits discovered on an earlier step.
+ static TypeMem priorEscapes(Type old) {
+ TypeMem mem = TypeMem.START;
+ if( CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Opto.ordinal() &&
+ CodeGen.CODE._phase.ordinal() <= CodeGen.Phase.Lower.ordinal() &&
+ old instanceof TypeMem prior )
+ mem = mem.makeFrom(XInt.meet(mem._escFs,prior._escFs),
+ XInt.meet(mem._escAs,prior._escAs));
+ return mem;
+ }
+
@Override
public Node idealize() {
int len = nIns();
diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/node/SummaryNode.java b/chapter28b/src/main/java/com/seaofnodes/simple/node/SummaryNode.java
index 426174e7..c22b4bdd 100644
--- a/chapter28b/src/main/java/com/seaofnodes/simple/node/SummaryNode.java
+++ b/chapter28b/src/main/java/com/seaofnodes/simple/node/SummaryNode.java
@@ -16,6 +16,7 @@ public class SummaryNode extends CFGNode implements MultiNode {
super(inputs);
_state = state;
_classDependencyFIDXs = classDependencyFIDXs;
+ CodeGen.CODE.add(CodeGen.CODE._globalEscapes);
}
@Override public String label() { return "Summary"; }
diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/chapter28b/src/main/java/com/seaofnodes/simple/type/TypeMem.java
index 9ae68bcf..d0fc3ee9 100644
--- a/chapter28b/src/main/java/com/seaofnodes/simple/type/TypeMem.java
+++ b/chapter28b/src/main/java/com/seaofnodes/simple/type/TypeMem.java
@@ -236,7 +236,10 @@ public class TypeMem extends Type {
: 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;
+ // An abstract pointer or scalar can refine to a concrete escaped
+ // object or function. Until then it must allow all of their bits.
+ case TypePtr ptr -> ptr.isHigh() ? xs : XInt.FULL;
+ case Type tt -> tt == BOTTOM || tt == TypeScalar.BOT ? XInt.FULL : xs;
};
}
@@ -247,7 +250,8 @@ public class TypeMem extends Type {
? 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;
+ case TypePtr ptr -> ptr.isHigh() ? xs : XInt.FULL;
+ case Type tt -> tt == BOTTOM || tt == TypeScalar.BOT ? XInt.FULL : xs;
};
}
}
diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter20Test.java
index 1001e691..66a318ae 100644
--- a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter20Test.java
+++ b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter20Test.java
@@ -216,7 +216,7 @@ for( int i=0; i<ary#-1; i++ )
return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6
""";
testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);");
- testCPU(src,"riscv" , "SystemV", 9,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));");
testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));");
}
diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
index d5b41ac9..f1dc54e2 100644
--- a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
+++ b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
@@ -324,7 +324,7 @@ for( int i=0; i<ary#-1; i++ )
return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6
""";
testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);");
- testCPU(src,"riscv" , "SystemV", 9,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));");
testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));");
}
diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/TypeTest.java b/chapter28b/src/test/java/com/seaofnodes/simple/TypeTest.java
index 113676ff..bb08b5ac 100644
--- a/chapter28b/src/test/java/com/seaofnodes/simple/TypeTest.java
+++ b/chapter28b/src/test/java/com/seaofnodes/simple/TypeTest.java
@@ -7,6 +7,23 @@ import static org.junit.Assert.assertSame;
public class TypeTest {
+ // Refining an unknown pointer must remove possible escapes, not add them.
+ // Inlining can expose the abstract self pointer before its concrete class.
+ @Test public void testAbstractPointerEscapes() {
+ TypeMemPtr object = TypeMemPtr.make(TypeStruct.make("EscapingCallback",false,
+ Field.make("callback",TypeFunPtr.TEST,2,false)));
+ for( Type precise : new Type[]{object,TypeFunPtr.TEST} )
+ for( Type broad : new Type[]{Type.BOTTOM,TypeScalar.BOT,TypePtr.PTR,TypePtr.NPTR} ) {
+ Assert.assertTrue(precise.isa(broad));
+ TypeMem unknown = TypeMem.START.escapes(broad);
+ assertSame(XInt.FULL,unknown._escFs);
+ assertSame(XInt.FULL,unknown._escAs);
+ Assert.assertTrue(TypeMem.START.escapes(precise).isa(unknown));
+ }
+ for( Type empty : new Type[]{Type.TOP,TypeScalar.TOP,TypePtr.XPTR,TypePtr.XNPTR,TypeNil.NIL} )
+ assertSame(TypeMem.START,TypeMem.START.escapes(empty));
+ }
+
@Test public void testBoundedBits() {
int[] sparse = XInt.make(XInt.make(2),65);
for( int[] bits : new int[][]{XInt.EMPTY,XInt.FULL,sparse,XInt.dual(sparse)} )
diff --git a/docs/roadmap.ja.md b/docs/roadmap.ja.md
index fdaaff91..ab791dc8 100644
--- a/docs/roadmap.ja.md
+++ b/docs/roadmap.ja.md
@@ -7,7 +7,7 @@
## 正確性とコンパイル
-- Eval2 の追加ロード時のスケジューリング用領域、`StopCUNode` の全呼び出し・関数要約の反復走査、Export 内に残るエンコード処理を整理する。
+- Eval2 の追加ロード時のスケジューリング用領域、`GlobalEscapeNode` が共有する呼び出し・関数要約の走査の増分化、Export 内に残るエンコード処理を整理する。
- 依存関係全体を調べ、変更を伝播してから保存 IR を再利用する、正しい増分ビルドを実装する。
- 型の読み込みからネイティブリンクまでのライブラリ変更を検出する。SHA-256 を含むシンボルの照合は将来案であり、Eval2 は整合したビルドを前提とする。
- コンパイル単位ごとの選択的ロードを、オブジェクト内の関数単位へ細分化する。
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 88f35654..cb2636b8 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -17,9 +17,10 @@ Remaining work:
arrays now retain capacity and reset only touched entries, but bitsets
still span previously allocated node IDs. Scheduling a newly loaded
object should cost only for that graph and any affected existing nodes.
-- Reduce repeated scans of all calls and unloaded function summaries in
- `StopCUNode.compute()`. Review its phase-dependent escape normalization and
- reliance on the previous result before changing propagation.
+- Make `GlobalEscapeNode`'s shared call/summary scan incremental. Compilation
+ unit Stops now reuse its result, but updating that result still scans all
+ calls and unloaded summaries. Review the phase-dependent escape normalization
+ and reliance on the previous result before changing propagation further.
- Move the remaining encoding fallback out of Export so Encoding prepares
all objects that Export will write.