From bb9b30b2ce6b5278e6d557b2bbb39ac3ec9406dc Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Sat, 7 Nov 2026 14:06:48 -0800 Subject: Partial objects allow field reading Still type safe, but broader usability for partial-init objects --- chapter29/README.md | 152 ++++++++++++++------ chapter29/docs/construction-groups.md | 139 +++++++++++++++--- .../java/com/seaofnodes/simple/codegen/Opto.java | 2 +- .../com/seaofnodes/simple/codegen/TypeCheck.java | 6 +- .../java/com/seaofnodes/simple/node/CallNode.java | 2 +- .../com/seaofnodes/simple/node/ConstructNode.java | 11 +- .../com/seaofnodes/simple/node/ConvertNode.java | 4 +- .../com/seaofnodes/simple/node/EscapeNode.java | 22 ++- .../java/com/seaofnodes/simple/node/FunNode.java | 2 + .../com/seaofnodes/simple/node/InitViewNode.java | 30 ++++ .../java/com/seaofnodes/simple/node/LoadNode.java | 12 +- .../java/com/seaofnodes/simple/node/MemOpNode.java | 2 +- .../java/com/seaofnodes/simple/node/ParmNode.java | 12 +- .../java/com/seaofnodes/simple/node/StoreNode.java | 2 + .../com/seaofnodes/simple/parser/Allocations.java | 2 +- .../com/seaofnodes/simple/parser/Construction.java | 56 ++++++-- .../com/seaofnodes/simple/parser/Functions.java | 5 +- .../com/seaofnodes/simple/parser/References.java | 2 +- .../com/seaofnodes/simple/type/TypeMemPtr.java | 19 +++ .../seaofnodes/simple/ConstructionGroupTest.java | 21 +-- .../com/seaofnodes/simple/PartialReadTest.java | 155 +++++++++++++++++++++ docs/roadmap.md | 53 ++++--- skills/chapter25-codex-notes.md | 13 ++ 23 files changed, 597 insertions(+), 127 deletions(-) create mode 100644 chapter29/src/main/java/com/seaofnodes/simple/node/InitViewNode.java create mode 100644 chapter29/src/test/java/com/seaofnodes/simple/PartialReadTest.java 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 errs = new Ary<>(Node.class); Ary constructs = new Ary<>(ConstructNode.class); + Ary 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=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 strs, HashMap types, IdentityHashMap 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 _print1(ExprPrinter 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 _print1(ExprPrinter p) { if( fun()._name!=null && fun()._name.endsWith("") && _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