summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-11-07 14:06:48 -0800
committerCliff Click <cliffc@acm.org>2026-11-07 14:06:48 -0800
commitbb9b30b2ce6b5278e6d557b2bbb39ac3ec9406dc (patch)
treecbe20a547d731ee7b712da7b8dd31b89ae93744c
parentce3b04603d7af995004291d9a7eb668f313e6772 (diff)
downloadSimple-bb9b30b2ce6b5278e6d557b2bbb39ac3ec9406dc.tar.gz
Simple-bb9b30b2ce6b5278e6d557b2bbb39ac3ec9406dc.zip
Partial objects allow field readingescape-flow
Still type safe, but broader usability for partial-init objects
-rw-r--r--chapter29/README.md152
-rw-r--r--chapter29/docs/construction-groups.md139
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/codegen/Opto.java2
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java6
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/node/CallNode.java2
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/node/ConstructNode.java11
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/node/ConvertNode.java4
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/node/EscapeNode.java22
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/node/FunNode.java2
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/node/InitViewNode.java30
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/node/LoadNode.java12
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/node/MemOpNode.java2
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/node/ParmNode.java12
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/node/StoreNode.java2
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/parser/Allocations.java2
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/parser/Construction.java56
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/parser/Functions.java5
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/parser/References.java2
-rw-r--r--chapter29/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java19
-rw-r--r--chapter29/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java21
-rw-r--r--chapter29/src/test/java/com/seaofnodes/simple/PartialReadTest.java155
-rw-r--r--docs/roadmap.md53
-rw-r--r--skills/chapter25-codex-notes.md13
23 files changed, 597 insertions, 127 deletions
diff --git a/chapter29/README.md b/chapter29/README.md
index 5dae8de6..db6e90fd 100644
--- a/chapter29/README.md
+++ b/chapter29/README.md
@@ -4,9 +4,15 @@ English | [日本語](README.ja.md)
[Previous: Chapter 28b — Separate Compilation](../chapter28b/README.md)
-This chapter makes the compiler easier to read and change. It starts by breaking
-the parser into grammar-sized pieces. Further cleanup belongs here; earlier
-chapters retain their teaching snapshots.
+This chapter makes the compiler easier to read and change. It starts by
+breaking the parser into grammar-sized pieces. Constructors now check field
+initialization with ordinary memory types, and can pass an incomplete `self` to
+other calls in a type-safe way.
+
+There is lots more cleanup here; earlier chapters are left alone instead of
+backporting the cleanups, in part to show the normal progression of large
+project development.
+
## Following the grammar
@@ -17,60 +23,118 @@ those responsibilities without introducing an AST: parsing still builds
Sea-of-Nodes IR directly.
`Parser` owns the source cursor, active scope, type registry, and common token
-and IR helpers. Each component shares that state through `Parser P`.
+and IR helpers. Each component shares that state through `Parser P`.
`Statements`, `Expressions`, `Declarations`, `Types`, `Functions`, and `Structs`
-follow the corresponding grammar productions. `References` handles name and
+follow the corresponding grammar productions. `References` handles name and
field resolution; `Allocations`, `Conversions`, and `ControlFlow` hold shared
-graph-building operations. `Lexer` owns tokenization and source locations, and
+graph-building operations. `Lexer` owns tokenization and source locations, and
`Var` records lexical bindings.
The [grammar and code map](docs/29-grammar.md) connects productions to methods.
Start with `Statements.parseStatement` or `Expressions.parseAssignment` and
-follow the calls alongside it. Deferred parsing remains explicit: `for` updates
+follow the calls alongside it. Deferred parsing remains explicit: `for` updates
are parsed after their bodies, and declared constructors after field discovery.
The [source layout](src/main/java/com/seaofnodes/simple/README.md) describes the
-other packages. `IterPeeps` now lives in `codegen/` beside the compilation passes.
+other packages. `IterPeeps` lives in `codegen/` beside the compilation passes.
+
## Checking constructor completion
-Instance fields use ordinary Loads and Stores during declaration initialization
-and constructor execution. One hidden private-memory value flows through scope
-merges and `addReturn`; field values are no longer copied into memory at exit.
-
-Fresh fields start at `TypeScalar.BOT`, which preserves a missing live arm at a
-merge. Forward memory types supply the initialization proof at Loads and merged
-Returns. Checks run at TypeCheck using the existing optimization passes; there
-is no extra construction solve, initialization walk, or field-state map.
-The earlier experiments are recorded in [the late-check notes](docs/deferred-construction.md).
-The [outside-world escape flow](docs/escape-flow.md) now restores the four
-recursive-class IR expectations and gives hidden class receivers their owning
-singleton as the unknown-caller default. The frozen public-interface inventory
-is removed; late imported identities remain a precision follow-up.
-Class singleton initialization
-retains its existing store-at-exit path.
-
-`ConstructorCompletionTest` covers partial assignments, early returns, loops,
-declaration defaults and immediate field access, and the formerly disabled
-required-number/nullable-default regression. The large comment on
+How constructors are handled has gone back and forth over the chapters; this
+testament to how implementing a language is a journey of exploration more than
+a rote rewriting of the grammer.
+
+Instance fields now use ordinary Loads and Stores throughout initialization.
+Instead of keeping field values in locals until construction ends, the
+constructor works on a real `self` object with private memory. This lets it
+lend `self` to another constructor or function while construction is underway.
+
+Fresh fields start at `TypeScalar.BOT`, which is the uninitialized indicator.
+An initialized integer with an unknown value has `TypeInteger.BOT`, distinct
+from the uninitialized `TypeScalar.BOT`, and similarly for other `BOT` types.
+Stores replace `TypeScalar.BOT` with that field's type in private memory.
+`TypeScalar.BOT` value survives merging at Phis and other control flow, and we
+use it to make sure a field is initialized on all paths.
+
+The completion form of `ConstructNode` checks that every required field is
+initialized on every constructor exit. Once the types prove completion and the
+node adds no pointer restriction, it folds away during optimization.
+Otherwise, `TypeCheck` asks it for an error. This uses the existing type flow
+and error walk; there is no extra construction pass.
+
+Loads check for loading `TypeScalar.BOT` (i.e., loading from uninitialized
+memory), and report an error.
+
+Constructors keep their private-memory and precise aliasing, only losing it
+after the constructor completes.
+
+
+## Incomplete Types
+
+A constructor can pass its partially constructed `self` to other constructors
+and functions. This permits doubly linked lists and parent/child tree nodes to
+cross-link during construction without reading uninitialized fields.
+
+To pass an incomplete type the receiving parameter must declare the partial
+qualifier `^`, for example a`val foo = { Point ^p -> ... }`. The function may
+forward `p`, retain it in another object's private construction memory, and
+read fields proven initialized by every caller. It cannot write through `p`,
+even to non-final fields, or store it into an ordinary object or global. The
+original constructor retains its write permission, including writes to final
+fields until construction finishes.
+
+
+```simple
+struct _Point {
+ int~ x; // final int x
+ int~ y; // final int y
+ new _Point = { int n -> x=n; y=_read(self); };
+};
+val _read = { _Point ^p -> return p.x; };
+return new _Point(arg).y;
+```
+
+Here `x` is readable during `_read`, while `y` is still uninitialized. The
+private names keep the callers within this compilation: unknown callers of a
+public `^` interface promise no initialized fields. If any live caller passes
+a partial without `x` set, the merged parameter loses that guarantee and the
+Load reports an error.
+
+A `new` object is *incomplete* when it starts life and if any of its actual
+arguments are incomplete. With only complete arguments, including null, the
+object is complete after construction. Otherwise it completes with the
+original object; the whole collection completes at once. This allows
+construction to assemble a cycle: after the root construction completes, the
+reachable objects can be used normally. Each constructor must still initialize
+its own fields before returning.
+
+`TypeMemPtr` carries the *partial* qualifier through ordinary type flow. An
+`InitViewNode` combines `self` with its current private memory to describe which
+fields are readable at that point: declared types for initialized fields,
+`TypeScalar.BOT` for the rest. Actual values still come from memory. Calls
+borrowing `self` carry its current Stores on their normal memory edge.
+
+Redundant views fold during optimization. Other views preserve the partial
+restriction or mask unreadable fields until `TypeCheck` checks their consumers
+and removes them. A surviving view is not itself an error: `_read` above needs
+only `x`, not a fully initialized object.
+
+These guarantees remain conservative. Reading a set object-reference field
+does not prove that its referent's fields are initialized. Arrays are already
+initialized and cannot contain partial references. Borrowed reads use aliasable
+memory, so allocating another object of the same class is allowed. Reading an
+initialized final field during construction does not freeze its value: the
+owning constructor may still change it afterward.
+
+Separate compilation of `^` interfaces remains unsupported. Inferred field
+requirements and nested initialization guarantees remain on the roadmap.
+
+The comment in
[`Construction`](src/main/java/com/seaofnodes/simple/parser/Construction.java)
-explains the type proof and the separate yes/no restriction on references.
-
-The [construction-group experiment](docs/construction-groups.md) adds `C ^arg`
-constructor parameters. A new result is incomplete iff an actual argument is
-incomplete. A `TypeMemPtr` qualifier propagates this through normal type flow;
-a temporary `ConstructNode` types each new result from that call's arguments.
-Only self may be read while incomplete; ordinary calls require completed
-objects. Field sharpening and inferred requirements are deferred,
-with their litmus tests preserved in `ConstructionRequirementsTest`. Separate
-compilation of constructors using `^` remains unsupported.
-
-## Further cleanup
-
-Source privacy is deferred. Leading underscores retain the existing export
-filtering behavior; this chapter does not yet enforce access restrictions on
-source uses. The [roadmap](../docs/roadmap.md) retains that proposal and the other
-language candidates.
+and the [construction notes](docs/construction-groups.md) explain the type flow,
+memory edges, and remaining restrictions.
+
## Build and test
diff --git a/chapter29/docs/construction-groups.md b/chapter29/docs/construction-groups.md
index 8dccdf6d..197a6082 100644
--- a/chapter29/docs/construction-groups.md
+++ b/chapter29/docs/construction-groups.md
@@ -18,7 +18,7 @@ One `#selfMem` binding carries this memory through scopes and returns. A Load
checks its incoming field type. Constructor completion checks the merged Return
memory; the explicit constructor receives the declaration initializer's merged
result, including early exits. There is no backwards initialization query,
-per-field parser state, call-site snapshot, or store-all instance exit.
+per-field parser state, or store-all instance exit.
The existing Iter/Opto passes propagate these types; TypeCheck reports errors.
There is no extra construction type solve or retained source-use list. An
@@ -40,22 +40,23 @@ fields retain their declared signatures in private summaries, avoiding repeated
unfolding of a constructor whose returned memory contains its own function field.
Class singleton initialization keeps its older store-at-exit implementation.
-## A yes/no rule for references
+## Partial references and readable fields
-`self` and incoming `C ^arg` constructor parameters are incomplete references.
-`C? ^arg` also accepts null. The qualifier is permitted only on struct parameters
-of explicit constructors, not on arrays or ordinary function parameters.
+`self` and incoming `C ^arg` parameters are incomplete references. `C? ^arg`
+also accepts null. Ordinary functions and constructors accept the qualifier on
+struct parameters; arrays cannot be qualified this way.
- An allocation result is incomplete if any actual argument is incomplete.
- With only completed arguments, including null, the result is complete.
- An incomplete reference may be stored into self or passed to a `^` parameter.
- Ordinary calls and stores into other objects require completed references.
-- Field lookup on an incomplete object is forbidden, except through self.
- Exact local aliases of self share its current private memory.
-- An object-reference field read from incomplete self is conservatively
- incomplete. There is no inference of initialized-field requirements on `^`
- arguments, nor permission to traverse a constructed member before its root
- construction finishes. Arrays are already initialized and cannot receive
+- A borrowed partial can read fields its incoming type proves initialized.
+ It cannot write fields, even non-finals. Exact local aliases of self retain
+ the original constructor's access to its current private memory.
+- An object-reference field read through a partial is conservatively
+ incomplete, with no initialized-field guarantees for its referent. There is
+ no inference of initialized-field requirements on `^` arguments.
+ Arrays are already initialized and cannot receive
incomplete elements, so an initialized array field can be used normally.
For example:
@@ -70,7 +71,7 @@ struct Parent {
int~ payload;
new Parent = { int n ->
child = new Child(self); // Incomplete result: self is incomplete.
- // child.parent is forbidden here.
+ // Retention does not authorize reading parent.payload through child.
payload = n;
};
};
@@ -83,8 +84,59 @@ Here `Test` names the source unit. After the root Parent constructor completes,
the entire cycle is usable: all members have initialized their own fields, and
none could expose an incomplete reference to ordinary code during construction.
The Child constructor still treats its `^par` as incomplete when the actual
-argument is complete; it may retain or forward it, but cannot read `par.payload`.
-Field sharpening and inferred contracts are deliberately deferred.
+argument is complete. This public constructor also has unknown callers, whose
+partial arguments promise no initialized fields, so `par.payload` is unreadable.
+
+## Forward field views
+
+At a use of self, `InitViewNode` combines the pointer with that program point's
+private memory. Its TypeMemPtr contains declared types for initialized fields
+and Scalar.BOT for missing fields. The node retains the declaration separately
+from the incoming flow shape, so an exact self alias captured before a Store
+can receive a fresh view afterward. Earlier calls retain their earlier views.
+
+For example, a private helper can read `x` while `y` is still unset:
+
+```simple
+struct _C {
+ int~ x;
+ int~ y;
+ new _C = { int n -> x=n; y=_read(self); };
+};
+val _read = { _C ^p -> return p.x; };
+return new _C(arg).y;
+```
+
+Parm meets the incoming shapes using the normal type flow. If any live caller
+lacks `x`, Scalar.BOT survives and the Load reports the missing initialization.
+Unknown callers supply an empty field promise. Parsing must also start borrowed
+parameters at that bound: otherwise early folding of `p.x*0` could hide a bad
+read before SCCP. The hidden constructor receiver retains its declaration;
+its private memory supplies its read checks and precise field updates.
+
+Values still come from normal memory. Before a call borrowing self, EscapeNode
+borrow slices carry the current constructor Stores onto the ordinary call
+memory edge. These slices are aliasable and do not publish the private identity
+or its fields into the global escape sets. No hidden partial-memory arguments,
+field bitsets, additional passes, or recursive flow queries are introduced.
+
+InitView folds before TypeCheck when its input already has the same type and
+retains a structural partial view. Nested partial-only Construct wrappers also
+collapse. A Parm's current partial type alone is insufficient: inlining may
+later substitute a complete actual argument, which must not grant the borrowed
+parameter permission to write or publish that object. The completion form of
+Construct already folds once its memory proves initialization and its result
+type matches its input.
+
+TypeCheck checks the consumers and erases remaining InitViews along with the
+checked Construct boundaries in its existing walk. A surviving field view is
+not necessarily erroneous: a reader may need only the initialized subset.
+Construct remains necessary for
+final-field completion and for the partial qualifier of each source `new`.
+Alias Phis and nested reference loads are conservative; they do not acquire
+the original constructor's write permission. This prototype adds no general
+ban on overlapping allocations of the same class: only the owning receiver
+uses private memory, while borrowed reads use aliasable memory.
## Aliases and loops
@@ -121,14 +173,69 @@ source-use lists or flow facts and performs no recursive Load/Phi/Call query. Th
root and allocation bitsets are gone. There are no field-requirement sets,
hidden `^` memory arguments, or runtime group objects.
-`ConstructionGroupTest` exercises cycles, complete/null arguments, prohibited
+`PartialReadTest` exercises field reads, missing fields, forwarding, merged
+callers, loops, exact aliases, constructor retention, and borrowed write and
+publication restrictions. `ConstructionGroupTest` exercises cycles, complete/null arguments, prohibited
lookups/publication, aliases, branch merges, complete and incomplete loop
backedges, and declared restrictions with only complete callers, including an
uninlined constructor through machine encoding. `ConstructorCompletionTest`,
`FinalConstructionTest`, and `BoundaryTest` exercise field-memory semantics.
The former inferred-requirement litmus tests are preserved, explicitly deferred,
-in `ConstructionRequirementsTest`. Separate compilation of `^` constructors is
+in `ConstructionRequirementsTest`. Separate compilation of `^` functions is
still deliberately unsupported. The pointer qualifier has an object-file
representation, but importing and checking the full construction contract is
outside this experiment. Rebuild chapter 29 object files after the type-format
change.
+
+## Deferred field-aware contracts (2026-11-07)
+
+Decision: do not implement inferred contracts now. Cliff considers the required
+infrastructure too large for the feature alone; revisit when a general
+backwards/liveness flow is justified, potentially for memory management.
+The later forward-field-view prototype above does not implement this contract
+discovery proposal. Separate compilation of `^` interfaces remains rejected.
+
+The intended extension would let functions and constructors receive partially
+initialized objects, read initialized fields, retain partial references with
+their restrictions, and write non-final fields. Only the original constructor
+would retain permission to initialize/reassign its own final fields; passing a
+pointer must not pass that permission. Final-field reads during construction
+would not establish value stability against later writes by that constructor.
+
+The proposed contract representation uses normal types, not a separate field
+bitset. For a function reading only `P.pi`, its unknown-caller default would be
+schematically `Partial{pi:declared-pi-type, name:TypeScalar.BOT}`. Scalar.BOT
+means no initialization guarantee, not a requirement that the caller leave the
+field unset. Required fields receive their declared types, covering every legal
+external caller. MEET with actual arguments preserves those guarantees only
+when the callers satisfy them; extra initialized fields supplied by locals do
+not strengthen the body beyond the external contract. TypeStruct/Field already
+meet field contents, and nested/cyclic types can represent nested contracts.
+TOP is not the missing-initialization marker.
+
+Cliff proposed discovering conservative, context-insensitive requirements after
+Iter and before SCCP. Freeze the resulting default argument types on functions;
+SCCP consumes them without refining the contracts. Reads on branches SCCP later
+eliminates may still impose requirements, as may reads following assignments.
+That rejection of some valid programs is acceptable for an opt-in `^` feature.
+Combine compatible requirement shapes with JOIN; combine possible caller states
+with MEET. Keep the contract distinct from the merged Parm type, and check each
+actual argument against it for a call-site diagnostic such as
+"foo requires Partial.pi to be initialized". Serialize contracts with declaration
+metadata so checking imported calls does not require loading bodies.
+
+The complication is discovering and propagating those contracts. Local SSA
+aliases share values; Phi demands reach every input. A demand on a loaded field
+becomes a nested receiver requirement. Calls substitute actual arguments for
+formals, and recursive forwarding needs a convergent propagation rule. Cyclic
+types represent recursive requirements but do not by themselves prevent a
+discovery algorithm from unfolding progressively deeper paths. Indirect calls
+need contracts available through their callable interfaces; returns need usable
+guarantees without retroactively strengthening an exported contract for a client.
+Stored references and helper writes also require a coherent account of private
+construction memory and object identity; that discussion was deliberately left
+unfinished. None of this requires H-M or bidirectional type inference, but it
+does require a substantial backwards demand analysis to handle well.
+
+The ignored ConstructionRequirementsTest retains useful litmus cases, not an
+approved implementation plan. Do not restart this work without a new request.
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/codegen/Opto.java b/chapter29/src/main/java/com/seaofnodes/simple/codegen/Opto.java
index f841e75b..6d653835 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/codegen/Opto.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/codegen/Opto.java
@@ -83,7 +83,7 @@ abstract public class Opto {
if( x instanceof ParmNode && x._type.isHighOrConst() ) deps.push(x);
// Construction identities may fold after the optimistic solve.
// Adds may need sorting after imported node IDs were assigned.
- if( x instanceof ConstructNode || x instanceof AddNode ) deps.push(x);
+ if( x instanceof ConstructNode || x instanceof InitViewNode || x instanceof AddNode ) deps.push(x);
// SCCP adds call edges and default arguments, not numeric
// operations. Gather unresolved modes on this required walk.
if( x instanceof ModeNode mode && mode.mode()==0 ) modes.push(x);
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java b/chapter29/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java
index 30e1415e..a86cd18a 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java
@@ -20,11 +20,13 @@ abstract class TypeCheck {
Ary<Node> errs = new Ary<>(Node.class);
Ary<ConstructNode> constructs = new Ary<>(ConstructNode.class);
+ Ary<InitViewNode> views = new Ary<>(InitViewNode.class);
code._stop.walk( n -> {
if( n.err() != null )
errs.add(n);
if( n instanceof CLInitNode ) code.add(n);
if( n instanceof ConstructNode con ) constructs.add(con);
+ if( n instanceof InitViewNode view ) views.add(view);
return null;
});
@@ -48,11 +50,13 @@ abstract class TypeCheck {
if( code._obs != null ) code._obs.error(min, min.err().getMessage());
throw min.err();
}
- // Checked construction boundaries have no runtime behavior. Gathered
+ // Checked construction boundaries and views have no runtime behavior. Gathered
// above during the existing error walk; no separate graph pass/solve.
for( ConstructNode con : constructs )
if( !con.isDead() ) con.subsume(con.in(1));
+ for( InitViewNode view : views )
+ if( !view.isDead() ) view.subsume(view.in(1));
}
private static int errorRank(Node n) {
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/node/CallNode.java b/chapter29/src/main/java/com/seaofnodes/simple/node/CallNode.java
index 552af499..91d27330 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/node/CallNode.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/node/CallNode.java
@@ -246,7 +246,7 @@ public class CallNode extends CFGNode {
// 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)) ) {
+ if( !arg(i+2)._type.isa(tfp.arg(i) instanceof TypeMemPtr p ? p.partialBound() : tfp.arg(i)) || !arg(i+2)._type.accessISA(tfp.arg(i)) ) {
if( arg(i+2)._type instanceof TypeMemPtr actual && actual._partial &&
tfp.arg(i) instanceof TypeMemPtr formal && !formal._partial )
return Parser.error("Cannot pass an incomplete object to a parameter without '^' (ordinary function or constructor)",_loc);
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/node/ConstructNode.java b/chapter29/src/main/java/com/seaofnodes/simple/node/ConstructNode.java
index 3984e48f..0af37a02 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/node/ConstructNode.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/node/ConstructNode.java
@@ -49,10 +49,17 @@ public class ConstructNode extends Node {
return ptr.withPartial(partial);
}
@Override public Node idealize() {
- // Wait for Opto to install closed field types. Afterwards, a proven
- // identity with initialized fields has no remaining error obligation.
+ // Restricting an already restricted view is redundant, including after
+ // inlining. A partial Parm type alone can later sharpen to complete.
+ if( _partial && partialView(in(1)) ) return in(1);
+ // Wait for Opto to install closed field types. Afterwards, a proven
+ // identity with initialized fields is good, and can be removed.
+ // Otherwise we're keeping this Node until we error report.
return !_partial && CodeGen.CODE._phase.ordinal()>=CodeGen.Phase.Opto.ordinal() && _type==in(1)._type && err()==null ? in(1) : null;
}
+ static boolean partialView(Node n) {
+ return n instanceof InitViewNode || n instanceof ConstructNode con && con._partial;
+ }
@Override public Parser.ParseException err() {
if( in(2)==null || in(2)._type.isHigh() || !(in(1)._type instanceof TypeMemPtr ptr) ) return null;
Type mem = in(2)._type;
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/node/ConvertNode.java b/chapter29/src/main/java/com/seaofnodes/simple/node/ConvertNode.java
index b9fbc4d2..7616932d 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/node/ConvertNode.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/node/ConvertNode.java
@@ -46,7 +46,9 @@ public class ConvertNode extends Node {
// Shape/access still follow the declaration: switching between
// the entire source and destination types is not monotone.
if( src instanceof TypeMemPtr actual && actual.notNull() ) ptr = (TypeMemPtr)ptr.nonZero();
- return ptr.withPartial(TypeMemPtr.mayBePartial(src));
+ ptr = ptr.withPartial(TypeMemPtr.mayBePartial(src));
+ return src instanceof TypeMemPtr actual && actual._partial && actual._obj._name==ptr._obj._name
+ ? ptr.initialized(actual._obj) : ptr;
}
if( src.isa(_dst) ) return src;
return _dst;
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/node/EscapeNode.java b/chapter29/src/main/java/com/seaofnodes/simple/node/EscapeNode.java
index f7a88df1..57340609 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/node/EscapeNode.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/node/EscapeNode.java
@@ -9,19 +9,26 @@ import java.util.HashMap;
import java.util.IdentityHashMap;
/**
- * Merge public and private aliases.
+ * Merge public and private aliases. A borrow orders partial-reader calls
+ * after constructor Stores without adding the private values to escape sets.
* 0 - control
* 1 - private pointer
* 2 - private alias
* 3 - public alias
*/
public class EscapeNode extends TypeNode {
+ public final boolean _borrow;
public EscapeNode(Field fld, Node self, Node priv, Node pub ) {
+ this(fld,self,priv,pub,false);
+ }
+ public EscapeNode(Field fld, Node self, Node priv, Node pub, boolean borrow ) {
super(fld,null,self,priv,pub);
+ _borrow = borrow;
}
public EscapeNode(EscapeNode esc ) {
super(esc); // Copy constructor
+ _borrow = esc._borrow;
// 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
@@ -38,6 +45,7 @@ public class EscapeNode extends TypeNode {
@Override public Tag serialTag() { return Tag.Escape; }
@Override public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) {
+ assert !_borrow; // Source ^ interfaces cannot yet be serialized.
baos.packed2(types.get(_con));
}
static Node make( BAOS bais, Type[] types) {
@@ -73,7 +81,14 @@ public class EscapeNode extends TypeNode {
// 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);
+ Type bound = fld._t;
+ if( _borrow && bound instanceof TypeMemPtr ptr && TypeMemPtr.mayBePartial(tpriv) )
+ bound = ptr.withPartial(true).partialBound();
+ Type t = tpriv.meet(tpub).join(bound);
+ // Lending memory to a local read-only partial argument is an ordering
+ // edge, not publication to unknown callers. Keep the public escape sets.
+ if( _borrow )
+ return TypeMem.make(fld._alias,t,false,false,false,mpub._escFs,mpub._escAs);
int[] fidxes = XInt.meet(mpriv._escFs, mpub._escFs);
int[] aliases = XInt.meet(mpriv._escAs, mpub._escAs);
// Keep escapes from the actual private value before narrowing it to
@@ -112,11 +127,12 @@ public class EscapeNode extends TypeNode {
}
@Override public boolean eq(Node n) {
+ if( _borrow!=((EscapeNode)n)._borrow ) return false;
if( fld()._alias!=((EscapeNode)n).fld()._alias )
return false;
assert _con == ((EscapeNode)n)._con;
return true;
}
- @Override int hash() { return _con.hashCode(); }
+ @Override int hash() { return _con.hashCode() ^ (_borrow ? 1024 : 0); }
}
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter29/src/main/java/com/seaofnodes/simple/node/FunNode.java
index 6422f7f1..8b6b347e 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/node/FunNode.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/node/FunNode.java
@@ -102,6 +102,8 @@ public class FunNode extends RegionNode {
// uniform call syntax, including callbacks forwarded by another class.
public Type defaultArg(int idx) {
Type t = sig().arg(idx);
+ if( t instanceof TypeMemPtr ptr && ptr._partial &&
+ !(idx==0 && sig().nargs()>1 && sig().arg(1) instanceof TypeMem) ) return ptr.partialBound();
if( idx!=0 || t!=TypePtr.PTR ) return t;
TypeStruct clz = (TypeStruct)Parser.TYPES.get(TypeStruct.addClzPrefix(_compunit._cname));
assert clz!=null;
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/node/InitViewNode.java b/chapter29/src/main/java/com/seaofnodes/simple/node/InitViewNode.java
new file mode 100644
index 00000000..46988a99
--- /dev/null
+++ b/chapter29/src/main/java/com/seaofnodes/simple/node/InitViewNode.java
@@ -0,0 +1,30 @@
+package com.seaofnodes.simple.node;
+
+import com.seaofnodes.print.ExprPrinter;
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.*;
+
+/** A program-point view of self. Memory proves which slots may be read;
+ * their values still come from ordinary Loads. Erased after TypeCheck. */
+public class InitViewNode extends TypeNode {
+ public InitViewNode(TypeMemPtr declared, Node self, Node mem) { super(declared,null,self,mem); }
+ @Override public String label() { return "InitView"; }
+ @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p("initView(").n(in(1)).p(")"); }
+ @Override public Type compute() {
+ Type ptr = in(1)._type, mem = in(2)._type;
+ if( ptr.isHigh() || mem.isHigh() ) return Type.TOP;
+ if( !(ptr instanceof TypeMemPtr p) ) return ptr;
+ TypeStruct state = mem instanceof TypeMem m && m._one && m._t instanceof TypeStruct ts
+ ? ts : p._obj.makeInit();
+ // An earlier alias can have fewer initialized slots. The declaration
+ // supplies field types; this program point's memory supplies the proof.
+ return p.makeFrom(((TypeMemPtr)_con)._obj).withPartial(true).initialized(state);
+ }
+ @Override public Node idealize() {
+ // Closed declarations and settled memory must prove this view redundant.
+ // Keep a structural partial restriction: a Parm's current partial type
+ // alone is not enough, since inlining can substitute a complete caller.
+ return CodeGen.CODE._phase.ordinal()>=CodeGen.Phase.Opto.ordinal() &&
+ _type==in(1)._type && ConstructNode.partialView(in(1)) ? in(1) : null;
+ }
+}
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/chapter29/src/main/java/com/seaofnodes/simple/node/LoadNode.java
index caede11f..85ab29f9 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/node/LoadNode.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/node/LoadNode.java
@@ -76,7 +76,6 @@ public class LoadNode extends MemOpNode {
return Type.BOTTOM; // Error in is error out
if( !(ptr0 instanceof TypeMemPtr tmp) )
return scalar(mem._t);
- if( tmp._partial && (!mem._one || mem._clz) ) return TypeScalar.BOT;
if( tmp.nullable() )
return Type.BOTTOM; // Load from nil-able is error
@@ -87,6 +86,8 @@ public class LoadNode extends MemOpNode {
if( pfld == null )
return tmp.isHigh() || tmp._obj.isHigh() ? TypeScalar.TOP : TypeScalar.BOT;
+ if( tmp._partial && (!mem._one || mem._clz) && pfld._t==TypeScalar.BOT ) return TypeScalar.BOT;
+
Type t = pfld._t;
boolean readonly = t instanceof TypeMemPtr view && view._ro;
// Load member of constant array
@@ -111,7 +112,10 @@ public class LoadNode extends MemOpNode {
t = t.makeRO();
// A completed receiver gives a completed view of its reachable fields.
// Through self, keep object references restricted until the root new.
- if( t instanceof TypeMemPtr value ) t = value.withPartial(tmp._partial && !value._obj.isAry());
+ if( t instanceof TypeMemPtr value ) {
+ value = value.withPartial(tmp._partial && !value._obj.isAry());
+ t = value._partial ? value.partialBound() : value;
+ }
return scalar(t);
}
@@ -338,7 +342,7 @@ public class LoadNode extends MemOpNode {
if( ptr instanceof ConvertNode conv && addDep(conv.val())._type instanceof TypeMemPtr src &&
src.isa(addDep(conv)._type) && src.accessISA(conv._type) )
ptr = conv.val();
- while( ptr instanceof ReadOnlyNode || ptr instanceof ConstructNode con && con._partial ) {
+ while( ptr instanceof InitViewNode || ptr instanceof ReadOnlyNode || ptr instanceof ConstructNode con && con._partial ) {
addDep(ptr);
ptr = ptr.in(1);
}
@@ -375,7 +379,7 @@ public class LoadNode extends MemOpNode {
Parser.ParseException err = super.err();
if( err!=null ) return err;
if( (in(0)==null || !in(0)._type.isHigh()) && _type==TypeScalar.BOT &&
- mem()._type instanceof TypeMem mem && mem._one && !mem._clz )
+ mem()._type instanceof TypeMem mem && ((mem._one && !mem._clz) || (ptr()._type instanceof TypeMemPtr p && p._partial)) )
return Parser.error("Field '"+_name+"' might not be initialized",_loc);
return null;
}
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/chapter29/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
index b453550d..35183380 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
@@ -123,7 +123,7 @@ public abstract class MemOpNode extends TypeNode {
// 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
- if( ptr instanceof TypeMemPtr p && p._partial &&
+ if( !_isLoad && ptr instanceof TypeMemPtr p && p._partial &&
!(mem()._type instanceof TypeMem m && m._one && !m._clz) )
return Parser.error("Cannot access field '"+_name+"' through an incomplete object",_loc);
// Better be a not-nil TMP
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/node/ParmNode.java b/chapter29/src/main/java/com/seaofnodes/simple/node/ParmNode.java
index 1b7aded1..aea402f6 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/node/ParmNode.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/node/ParmNode.java
@@ -20,7 +20,7 @@ public class ParmNode extends PhiNode {
super(label, inputs);
_idx = idx;
_declaredType = declaredType;
- _type = declaredType;
+ _type = declaredBound();
}
public ParmNode(ParmNode parm) { super(parm, parm._label ); _idx = parm._idx; _declaredType = parm._declaredType; }
@Override public Tag serialTag() { return Tag.Parm; }
@@ -49,6 +49,12 @@ public class ParmNode extends PhiNode {
public FunNode fun() { return (FunNode)in(0); }
+ // The hidden constructor receiver has private memory for checking reads.
+ // Only borrowed source parameters need an initially empty field promise.
+ private Type declaredBound() {
+ return !(_idx==2 && "self".equals(_label)) && _declaredType instanceof TypeMemPtr ptr ? ptr.partialBound() : _declaredType;
+ }
+
@Override
protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) {
if( fun()._name!=null && fun()._name.endsWith("<clinit>") && _label.equals("arg") )
@@ -68,7 +74,7 @@ public class ParmNode extends PhiNode {
return region()._type==Type.XCONTROL || region()._type==Type.TOP ? (_declaredType 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;
+ return declaredBound();
// Set type to local top of the starting type
Type t = Type.TOP;
for( int i = 1; i < nIns(); i++ ) {
@@ -93,7 +99,7 @@ public class ParmNode extends PhiNode {
// 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();
- t = t.join(_declaredType);
+ t = t.join(declaredBound());
return t==Type.TOP && _declaredType instanceof TypeMem ? TypeMem.TOP : t;
}
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/chapter29/src/main/java/com/seaofnodes/simple/node/StoreNode.java
index 2574d801..010d5f61 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/node/StoreNode.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/node/StoreNode.java
@@ -331,6 +331,8 @@ public class StoreNode extends MemOpNode {
public Parser.ParseException err() {
Parser.ParseException err = super.err();
if( err != null ) return err;
+ if( !_init && ptr()._type instanceof TypeMemPtr p && p._partial )
+ return Parser.error("Cannot modify a field through an incomplete object outside its constructor",_loc);
if( TypeMemPtr.mayBePartial(val()._type) &&
!(mem()._type instanceof TypeMem mem && mem._one && !mem._clz) )
return Parser.error("Cannot store an incomplete object into an ordinary object",_loc);
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/parser/Allocations.java b/chapter29/src/main/java/com/seaofnodes/simple/parser/Allocations.java
index 0eda2c62..be68e10e 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/parser/Allocations.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/parser/Allocations.java
@@ -150,7 +150,7 @@ final class Allocations {
new ProjNode(P._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();
+ parms[i] = new ParmNode("arg"+i,i+2,atypes[i],fun,ConstantNode.seed(fun.defaultArg(i)).peephole()).peephole();
NewNode nnn = new NewNode(ts,fun,ConFldOffNode.make(ts," len")).init();
Node self = new ProjNode(nnn,0,ts.str()).init();
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/parser/Construction.java b/chapter29/src/main/java/com/seaofnodes/simple/parser/Construction.java
index 2720ad3d..6dec8ac1 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/parser/Construction.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/parser/Construction.java
@@ -2,6 +2,7 @@ package com.seaofnodes.simple.parser;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
/**
* Field initialization is ordinary forward typing: New supplies Scalar.BOT
@@ -10,17 +11,18 @@ import com.seaofnodes.simple.type.*;
* initialized integer. Loads and merged Returns inspect these types after the
* parser closes loops and compute() stabilizes. No backwards initialization walk.
*
- * References have just a yes/no restriction. Constructor self and ^ parameters
- * are incomplete. An allocation result is incomplete iff an actual argument is
- * incomplete. Copies and Phis preserve this restriction. Only self may be read
- * while incomplete; object-reference fields read from self conservatively remain
- * incomplete. Arrays are born initialized and cannot contain incomplete refs.
- * Ordinary calls and stores outside self require complete references.
+ * Constructor self and ^ parameters are incomplete. InitViewNode combines self
+ * with its current private memory: set fields carry their declared types and
+ * missing fields carry Scalar.BOT. Local ^ parameters meet these shapes through
+ * ordinary forward flow; unknown callers promise no initialized fields. Only
+ * the original constructor may write through an incomplete pointer.
*
- * This permits cycles to be assembled but not traversed until the root new
- * returns. A new with only complete arguments, including null, returns complete.
- * A ^ formal is always restricted inside its constructor, even when a caller
- * passes a complete argument. No field requirements are inferred.
+ * Calls borrowing self receive its Stores on the ordinary public memory edge,
+ * using aliasable Escape slices without publishing its identity. Field values
+ * come from memory, not the pointer's initialization shape. Incomplete values
+ * may be retained only in private construction memory. Nested object references
+ * conservatively promise no initialized fields; arrays are born initialized.
+ * No field requirements are inferred and no extra memory arguments are added.
*
* Incompleteness lives in TypeMemPtr, not a parser provenance graph. Ordinary
* Phis carry it around loops, conversions preserve it, and ConstructNode types
@@ -39,7 +41,7 @@ import com.seaofnodes.simple.type.*;
* Reading a pointer field through a completed receiver gives a completed view
* of that field too: its object's construction could only retain incomplete
* references from the enclosing construction. There is no group ID to clear.
- * A read through incomplete self conservatively returns an incomplete reference
+ * A read through an incomplete pointer conservatively returns an incomplete reference
* (except initialized arrays), even when the stored value happens to be complete.
* Own-self access is identified by the receiver and its private memory, not by
* the nominal class: a ^ argument of the same class cannot use that exception.
@@ -53,7 +55,8 @@ import com.seaofnodes.simple.type.*;
* Hidden helpers have only allocator callers, while the public constructor
* binding denotes the allocator. Private-memory Parms therefore need no
* fictitious external default or constructor-specific transfer function.
- * There is no extra construction pass; see docs/deferred-construction.md.
+ * InitView and Construct are erased by the existing TypeCheck walk. There is
+ * no extra construction pass; see docs/construction-groups.md.
*/
final class Construction {
private final Parser P;
@@ -83,8 +86,33 @@ final class Construction {
return P._scope.in(P._scope.update(mem,null));
}
+ Node view(Node ptr) {
+ return own(ptr) ? new InitViewNode((TypeMemPtr)((ParmNode)_frame._self)._declaredType,ptr,selfMem()).peephole() : ptr;
+ }
+
+ // Carry the constructor's Stores on the call's ordinary memory edge.
+ // The borrowed receiver is read-only; this does not publish its identity.
+ Node callMemory(Ary<Node> args, Node pub) {
+ if( !_frame._owner ) return pub;
+ boolean lends = false;
+ for( int i=2; i<args.size()-1; i++ ) lends |= own(args.at(i));
+ if( !lends ) return pub;
+ Node self = _frame._self;
+ TypeStruct ts = (TypeStruct)Parser.TYPES.get(((TypeMemPtr)self._type)._obj._name);
+ Node priv = selfMem().keep();
+ for( Field fld : ts._fields ) {
+ pub.keep();
+ Node loan = new EscapeNode(fld,self,priv,pub,true).peephole();
+ Node next = P.mergeAlias(pub,fld._alias,loan);
+ pub.unkeep();
+ pub = next;
+ }
+ priv.unkeep();
+ return pub;
+ }
+
boolean own(Node ptr) {
- while( ptr instanceof ConvertNode || ptr instanceof ReadOnlyNode || ptr instanceof GuardNode || (ptr instanceof ConstructNode con && con._partial) ) ptr = ptr.in(1);
+ while( ptr instanceof InitViewNode || ptr instanceof ConvertNode || ptr instanceof ReadOnlyNode || ptr instanceof GuardNode || (ptr instanceof ConstructNode con && con._partial) ) ptr = ptr.in(1);
return _frame._owner && ptr==_frame._self;
}
@@ -97,6 +125,6 @@ final class Construction {
void checkSerializable() {
if( _borrowed )
- throw P.error("Separate compilation of constructors with '^' parameters is not supported yet");
+ throw P.error("Separate compilation of functions with '^' parameters is not supported yet");
}
}
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/parser/Functions.java b/chapter29/src/main/java/com/seaofnodes/simple/parser/Functions.java
index 5dd66730..14dee33b 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/parser/Functions.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/parser/Functions.java
@@ -74,8 +74,8 @@ final class Functions {
Type t = P.types.parseType(); // Arg type
if( t==null ) break;
if( P.match("^") ) {
- if( !constructor || !(t instanceof TypeMemPtr ptr) || ptr._obj.isAry() )
- throw P.error("'^' requires a struct parameter of a constructor");
+ if( !(t instanceof TypeMemPtr ptr) || ptr._obj.isAry() )
+ throw P.error("'^' requires a struct parameter");
t = ptr.withPartial(true);
P.construction._borrowed = true;
}
@@ -131,6 +131,7 @@ final class Functions {
}
// Into the call
+ args.set(1,P.construction.callMemory(args,args.at(1)));
CallNode call = (CallNode)new CallNode(site, args.asAry()).peephole();
// Post-call setup
CallEndNode cend = (CallEndNode)new CallEndNode(call,TypeRPC.constant(P._code.rpc(P._ref._cname))).peephole();
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/parser/References.java b/chapter29/src/main/java/com/seaofnodes/simple/parser/References.java
index 737ba4c9..483bd7a8 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/parser/References.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/parser/References.java
@@ -94,7 +94,7 @@ final class References {
int selfx = P._scope.kindFcn(var)._lexSize;
// Might be a method call from inside a method, so no explicit "self".
// Pass the in-scope "self"
- return P.expressions.parsePostfixMethod(rvalue,P._scope.in(P._scope.update(P._scope.var(selfx),null)));
+ return P.expressions.parsePostfixMethod(P.construction.view(rvalue),P._scope.in(P._scope.update(P._scope.var(selfx),null)));
}
// Assign-update direct into Scope
if( var._final && !P._scope.canInit(var) )
diff --git a/chapter29/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java b/chapter29/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java
index 943710d1..d9e11f8f 100644
--- a/chapter29/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java
+++ b/chapter29/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java
@@ -91,6 +91,25 @@ public class TypeMemPtr extends TypeNil {
return partial==_partial ? this : make(_nil,_obj,_one,_pub,_ro,partial);
}
+ // A partial declaration promises no initialized slots. Actual callers may
+ // supply a sharper shape; the declaration must not manufacture those facts.
+ public TypeMemPtr partialBound() { return _partial ? makeFrom(_obj.makeInit()) : this; }
+
+ // Project field initialization onto declared field types, not stored
+ // constants. A pointer field being set does not complete its referent.
+ public TypeMemPtr initialized(TypeStruct state) {
+ TypeStruct shape = _obj;
+ for( Field fld : _obj._fields ) {
+ Field actual = state.field(fld._fname);
+ Type t = actual==null ? TypeScalar.BOT : actual._t;
+ if( t==Type.BOTTOM || t==TypeScalar.BOT ) t = TypeScalar.BOT;
+ else if( t.isHigh() ) t = TypeScalar.TOP;
+ else t = fld._t==Type.BOTTOM ? TypeScalar.BOT : fld._t==Type.TOP ? TypeScalar.TOP : fld._t;
+ shape = shape.replace(fld.makeFrom(t));
+ }
+ return makeFrom(shape);
+ }
+
// Conservative on unresolved scalar/pointer families; null is complete.
public static boolean mayBePartial(Type t) {
return t instanceof TypeMemPtr ptr ? ptr._partial
diff --git a/chapter29/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java b/chapter29/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java
index b5bbfce8..e544fcad 100644
--- a/chapter29/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java
+++ b/chapter29/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java
@@ -41,10 +41,11 @@ public class ConstructionGroupTest {
}
@Test public void testIncompleteChildReadAfterInlining() {
- // This lookup supplies a live result. Inlining must not turn self
- // complete inside the body before checking the use of child.
- rejects(parent("child = new Child(self); payload = child.parent==null;")+
- "return new Parent(arg).payload;","incomplete object");
+ // Child's own slots are initialized, even though its parent remains
+ // partial. Reading that slot does not complete the parent reference.
+ String src = parent("child = new Child(self); payload = child.parent==null;")+
+ "return new Parent(arg).payload;";
+ assertEquals("0",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),0));
}
@Test public void testInitializerFeedsConstructor() {
@@ -61,14 +62,14 @@ public class ConstructionGroupTest {
@Test public void testNoIncompleteLookupOrEscape() {
rejects(parent("payload = 1; child = new Child(self); read(child);")+"return 0;","ordinary function");
rejects(parent("payload = 1; child = new Child(self);").replace("parent = par;","parent = par; value = par.payload;")+
- "return 0;","incomplete object");
+ "return 0;","Might be null");
rejects("""
struct Box { Test.C? value; }; val box = new Box;
struct C { int~ x; new C = { -> box.value = self; x = 1; }; };
return 0;
""","ordinary object");
rejects("struct C { C next = self; int~ x; new C = { -> x = next.x; }; }; return new C().x;",
- "incomplete object");
+ "might not be initialized");
}
@Test public void testAliasesAndLoopBackedges() {
@@ -82,8 +83,8 @@ public class ConstructionGroupTest {
"return 0;","ordinary function");
}
- @Test public void testConstructorOnlyQualifier() {
- rejects("struct C {}; val f = { C ^c -> return 0; }; return 0;","constructor");
+ @Test public void testPartialQualifier() {
+ new CodeGen("struct C {}; val f = { C ^c -> return 0; }; return 0;").driver(CodeGen.Phase.TypeCheck);
rejects("struct C { new C = { int ^x -> }; }; return 0;","struct parameter");
rejects(parent("payload = 1; child = new Child(self);").replace("^par","par")+"return 0;","without '^'");
CodeGen code = new CodeGen(parent("payload = 1; child = new Child(self);")+"return 0;")
@@ -127,7 +128,7 @@ public class ConstructionGroupTest {
}
@Test public void testDeclaredQualifierAndOwnReceiver() {
- // A complete caller does not grant permission to read a ^ formal.
+ // A public constructor's unknown callers promise no initialized slots.
for( String body : new String[]{"value = par.payload;", "value = par.payload*0;",
"int x = par.payload; value = x*0;", "value = par.payload==par.payload;"} )
rejects("""
@@ -137,7 +138,7 @@ public class ConstructionGroupTest {
new Child = { Parent ^par -> BODY };
};
return new Child(new Parent).value;
- """.replace("BODY",body),"incomplete object");
+ """.replace("BODY",body),"might not be initialized");
// The self exception belongs to the receiver, not every pointer of its class.
rejects("""
struct C {
diff --git a/chapter29/src/test/java/com/seaofnodes/simple/PartialReadTest.java b/chapter29/src/test/java/com/seaofnodes/simple/PartialReadTest.java
new file mode 100644
index 00000000..a6230b69
--- /dev/null
+++ b/chapter29/src/test/java/com/seaofnodes/simple/PartialReadTest.java
@@ -0,0 +1,155 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.node.InitViewNode;
+import com.seaofnodes.simple.node.ConstructNode;
+import com.seaofnodes.simple.parser.Parser;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class PartialReadTest {
+ private static String program(String body, String helper) {
+ return "struct _C { int~ x; int~ y; new _C = { int n -> " + body +
+ " }; }; val _read_noInline = { _C ^p -> " + helper +
+ " }; return new _C(arg).y;";
+ }
+ private static void rejects(String source, String message) {
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Accepted " + source);
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains(message));
+ }
+ }
+ @Test public void testLocalRead() {
+ String src = program("x=n; y=_read_noInline(self);", "return p.x;");
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertNull(code._stop.walk(n -> n instanceof InitViewNode ? n : null));
+ assertEquals("42",Eval2.eval(code,42));
+ }
+ new CodeGen(src).driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION,true,false,0);
+ }
+ @Test public void testRedundantViewsFoldBeforeTypeCheck() {
+ String src=program("x=n; y=0; y=_read_noInline(self);", "return p.x;");
+ CodeGen code=new CodeGen(src).driver(CodeGen.Phase.IterPostOpto);
+ assertNull(code._stop.walk(n -> n instanceof InitViewNode ? n : null));
+ assertNull(code._stop.walk(n -> n instanceof ConstructNode c && !c._partial ? n : null));
+ code.driver(CodeGen.Phase.TypeCheck);
+ assertEquals("31",Eval2.eval(code,31));
+
+ // Here the view still masks y. A valid program need not initialize
+ // every field before lending self, so survival is not itself an error.
+ code=new CodeGen(program("x=n; y=_read_noInline(self);", "return p.x;"))
+ .driver(CodeGen.Phase.IterPostOpto);
+ assertNotNull(code._stop.walk(n -> n instanceof InitViewNode ? n : null));
+ code.driver(CodeGen.Phase.TypeCheck);
+ assertEquals("31",Eval2.eval(code,31));
+ }
+ @Test public void testInliningKeepsBorrowRestriction() {
+ rejects("""
+ struct _C {
+ int !x;
+ new _C={ int n -> x=n; if(!n) _write(self); };
+ };
+ val _write={ !_C ^p -> if(p.x) p.x=9; return p.x; };
+ val a=new _C(0); val b=new _C(1);
+ return _write(b)+a.x;
+ ""","incomplete object");
+ }
+ @Test public void testMissingAndBranchField() {
+ for( String use : new String[]{"p.x", "p.x*0", "p.x==p.x"} )
+ rejects(program("y=_read_noInline(self); x=n;", "return "+use+";"),"might not be initialized");
+ rejects(program("if(n) x=n; y=_read_noInline(self); x=0;", "return p.x;"),"might not be initialized");
+ }
+ @Test public void testActualValueFromMemory() {
+ for( String body : new String[]{"x=1; val p=self; x=2; y=_read_noInline(p);",
+ "val p=self; x=2; y=_read_noInline(p);"} ) {
+ CodeGen code = new CodeGen(program(body,"return p.x;")).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("2",Eval2.eval(code,0));
+ }
+ }
+ @Test public void testBorrowedWrite() {
+ rejects(program("x=n; y=_read_noInline(self);", "p.x=9; return p.x;")
+ .replace("int~ x", "int !x").replace("_C ^p", "!_C ^p"),"incomplete object");
+ }
+ @Test public void testUnknownCallerCannotRead() {
+ rejects(program("x=n; y=_read_noInline(self);", "return p.x;")
+ .replace("_read_noInline","read_noInline"),"might not be initialized");
+ }
+ @Test public void testForwardingAndMergedCallers() {
+ String src = program("x=n; y=_read_noInline(self);", "return _next_noInline(p);")
+ + " val _next_noInline = { _C ^p -> return p.x; };";
+ assertEquals("17",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),17));
+ rejects(program("y=_read_noInline(self); x=n; y=_read_noInline(self);", "return p.x;"),"might not be initialized");
+ }
+ @Test public void testLoops() {
+ String src = program("x=1; for(int i=0; i<n; i++) x+=2; y=_read_noInline(self);", "return p.x;");
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("7",Eval2.eval(code,3));
+ assertEquals("1",Eval2.eval(code,0));
+ }
+ rejects(program("for(int i=0; i<n; i++) x=n; y=_read_noInline(self); x=0;", "return p.x;"),"might not be initialized");
+ }
+ @Test public void testNoPublication() {
+ rejects("struct Box { Test._C? !p; }; val box=new Box; " +
+ program("x=n; y=_read_noInline(self);", "box.p=p; return p.x;"),"ordinary object");
+ }
+ @Test public void testConstructorReadAndRetention() {
+ String src = """
+ struct _Child {
+ Test._Parent parent;
+ int~ seen;
+ new _Child = { Test._Parent ^p -> parent=p; seen=p.x; };
+ };
+ struct _Parent {
+ int~ x;
+ _Child child;
+ new _Parent = { int n -> x=n; child=new _Child(self); };
+ };
+ val p=new _Parent(arg); return p.child.seen+p.child.parent.x;
+ """;
+ assertEquals("22",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),11));
+ rejects(src.replace("x=n; child=new _Child(self);","child=new _Child(self); x=n;"),"might not be initialized");
+ }
+ @Test public void testInlineReadAndWrite() {
+ String src=program("x=n; y=_read_noInline(self);","return p.x;").replace("_noInline","");
+ assertEquals("19",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),19));
+ rejects(src.replace("return p.x;","p.x=9; return p.x;").replace("int~ x","int !x").replace("_C ^p","!_C ^p"),"incomplete object");
+ rejects(src.replace("x=n; y=", "y=").replace("(self);", "(self); x=n;"),"might not be initialized");
+ }
+ @Test public void testArrayAndNestedReference() {
+ String src="""
+ struct _C {
+ int[] data;
+ int~ y;
+ new _C = { int n -> data=new int[1]; data[0]=n; y=_read_noInline(self); };
+ };
+ val _read_noInline={ _C ^p -> return p.data[0]; };
+ return new _C(arg).y;
+ """;
+ assertEquals("23",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),23));
+ // A set reference slot is not proof that its referent is initialized.
+ rejects("struct D { int x=7; }; "+src.replace("int[] data","D data")
+ .replace("data=new int[1]; data[0]=n;","data=new D;")
+ .replace("p.data[0]","p.data.x"),"might not be initialized");
+ }
+ @Test public void testCompleteBorrowCannotPublishPartial() {
+ rejects("""
+ struct _C { int x=7; };
+ struct Box { _C? p; }; val box=new Box;
+ val _store={ !_C ^dst, _C ^src -> box.p=src; return dst.x; };
+ return _store(new _C,new _C);
+ ""","ordinary object");
+ }
+ @Test public void testSameClassMemory() {
+ String src=program("x=n; y=n ? _read_noInline(self) : x;",
+ "val other=new _C(0); return p.x+other.y;");
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("29",Eval2.eval(code,29));
+ assertEquals("0",Eval2.eval(code,0));
+ }
+ }
+}
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 0fa3933b..ea321761 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -3,7 +3,7 @@
English | [日本語](roadmap.ja.md)
Open compiler work and language proposals, reviewed against the current
-snapshots on 2026-11-01. These are future changes, not promises about the
+snapshots on 2026-11-07. These are future changes, not promises about the
language implemented by the tutorial. Language changes still need their own
design decisions and chapter boundaries.
@@ -13,13 +13,12 @@ design decisions and chapter boundaries.
components, with `Var` in `parser/` and `IterPeeps` in `codegen/`. It is the
workspace for further cleanup; Chapter 28b retains the preceding compiler.
-Constructor completion now checks the helper Returns after parsing, with missing
-fields retained in private memory instead of separate exit maps.
-The [construction-group prototype](../chapter29/docs/construction-groups.md)
-adds unfinished constructor parameters and inferred field requirements. Remaining
-work includes contracts for reference paths, exported contracts for separate
-compilation, and forward calls with unfinished arguments. A disabled completion
-test records the existing `TOP`-marker/inlining failure beside nullable defaults.
+Constructor completion now uses private-memory field types and the existing
+TypeCheck walk, after Iter/Opto propagation. The
+[incomplete-reference design](../chapter29/docs/construction-groups.md) uses a
+partial qualifier plus forward field views for local read-only borrowers.
+Inferred field requirements, nested guarantees, and imported construction
+contracts remain longer-term proposals.
Keep the recursive-descent path aligned with the grammar. Review moving `TYPES`
to `CodeGen`. Consider whether
an allocation-free use of try-with-resources makes keep/unkeep pairing clearer;
@@ -30,6 +29,13 @@ implementation and tests are experiments, not part of Chapter 29's behavior.
## Compilation and optimization
+### Better error messages
+
+Improve compiler diagnostics: identify the offending source expression, explain
+what was expected and what was found, and point to relevant declarations or call
+sites. Preserve useful source locations through optimization and imports, and
+avoid cascading errors that obscure the original problem.
+
### Phase ordering and incremental analysis
Remaining work:
@@ -161,19 +167,6 @@ remain legal. String spill fixtures with helpers outside `_String` accessing
keep those fixtures unchanged. Validate native execution and imported source
ownership alongside the access diagnostics.
-### Constructor helpers and completion
-
-Local SSA field updates, including final fields, repeated assignments, and
-loops, are implemented. The remaining question is broader: should constructors
-be allowed to pass unfinished `self` to helpers that become safe after
-inlining, and should completion checks wait until SCCP?
-
-Decide those rules before changing the current parser-time completion checks
-and early-publication restrictions. Separate reading an initialized field of
-an unfinished object from exposing that object to arbitrary code. The
-construction/escape teaching boundary can then be reviewed against the chosen
-semantics.
-
### ELF visibility and unknown callers
Escape analysis tracks functions reachable by unknown callers, while the ELF
@@ -229,11 +222,27 @@ ownership, borrowing, cycles, and native retention. Compact storage alone
does not establish those rules. Preserve static class images and explicit
native boundary conversions when choosing the representation.
-## Data and library proposals
+## Longer-term language and library proposals
These are language-design alternatives. The sketches below are not executable
examples of the current Simple grammar.
+### Inferred partial-initialization contracts — deferred
+
+Allow partial arguments to expose initialized fields, retain partial references,
+and propagate nested initialization guarantees, while reserving final-field
+writes to the original constructor. Cliff decided against implementing this
+now (2026-11-07): tracing requirements through aliases, nested fields, forwarding,
+recursive calls, and returned/stored references adds more infrastructure than
+is justified for this feature alone.
+
+Revisit alongside a general backwards/liveness analysis, which may also serve
+future memory management. The normal-type contract proposal and its unresolved
+details are recorded in the
+[construction notes](../chapter29/docs/construction-groups.md#deferred-field-aware-contracts-2026-11-07).
+The forward-only prototype permits local reads of proven initialized fields;
+it adds no inferred contracts. Keep the separate-compilation restriction meanwhile.
+
### Automatic memory management
Explore automatic memory reclamation with few explicit lifetime annotations,
diff --git a/skills/chapter25-codex-notes.md b/skills/chapter25-codex-notes.md
index bd68060a..4aabbb54 100644
--- a/skills/chapter25-codex-notes.md
+++ b/skills/chapter25-codex-notes.md
@@ -18,6 +18,19 @@ The [incremental build design](incremental-builds.md) expands the roadmap's
dependency and output-ownership requirements.
For shared viewer work, read [the graph viewer notes](graph-viewer-codex-notes.md).
+## Partial-initialization decision (2026-11-07)
+
+Cliff declined inferred partial-initialization contracts for now. The normal-type,
+pre-SCCP contract proposal is recorded in
+[the construction notes](../chapter29/docs/construction-groups.md#deferred-field-aware-contracts-2026-11-07).
+Revisit alongside general backwards/liveness analysis, potentially for memory
+management; do not treat the discussion or ignored requirement tests as approval
+to implement it. The roadmap places it among longer-term proposals. Cliff later
+authorized the narrower forward-only prototype: InitView carries initialized
+field shapes, existing Iter/SCCP propagates them, and TypeCheck checks and erases
+the views. Borrowed partials can read proven fields but cannot write; unknown
+callers promise no initialized fields. No new passes or backward demand walks.
+
## Restart handoff (2026-11-06)
Cliff requested this handoff before restarting. Active branch is `escape-flow`,