summaryrefslogtreecommitdiff
path: root/chapter29/docs/construction-groups.md
blob: 197a608237bcfe919e7af2060394de4f91a9de29 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# Construction: field types and incomplete references

Instance constructors use ordinary field Loads and Stores. There are two checks:
whether a field has a value, and whether an object reference is usable outside
construction. Neither requires a collection of constructor-group identities.

## Field initialization

New produces private `TypeMem` containing the object's `TypeStruct`, with
`TypeScalar.BOT` for fresh fields. Defaults emit ordinary Stores: zero for an
ordinary integer, null for a nullable pointer, no Store for a required field.
An assignment strongly updates its field in private memory. Phi meets the
incoming memories; a missing live arm survives because `BOT.meet(7) == BOT`.
An initialized unknown integer has `TypeInteger.BOT`, not `TypeScalar.BOT`.
Arrays retain their language-level zero filling.

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, 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
initialized field carried unchanged around a loop needs the optimistic SCCP
solution. Numeric fields in loop memory widen as scalar loop values do.
Arithmetic, conversions, and numeric Stores preserve Scalar.BOT, so a live bad
read cannot disappear merely because it is multiplied by zero. Genuinely unused
reads may be removed by DCE.

The allocator's ConstructNode retains the constructor's final private memory
for its completion check. TypeCheck gathers these nodes during its existing
error walk and removes checked boundaries before lowering. The pass removal
exposes known failures; see [the late-check notes](deferred-construction.md).

Internal construction memory parameters use actual allocation/initializer call
edges; the hypothetical external caller does not supply their memory. They
retain their memory family when there are no live callers. Function-valued
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.

## Partial references and readable fields

`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.
- 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:

```simple
struct Child {
    Test.Parent? parent;
    new Child = { Test.Parent? ^par -> parent = par; };
};
struct Parent {
    Child child;
    int~ payload;
    new Parent = { int n ->
        child = new Child(self); // Incomplete result: self is incomplete.
        // Retention does not authorize reading parent.payload through child.
        payload = n;
    };
};
val parent = new Parent(arg);
val local = new Child(parent);  // Complete argument, complete result.
return local.parent==parent;    // Lookup is allowed.
```

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. 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

Incomplete is a property of a value, not just the current contents of a parser
Var. Consider `while (n--) { use(p); p = self; }`: the backedge introduces an
incomplete value after `use(p)` has been parsed. `TypeMemPtr._partial` carries
that restriction through ordinary forward typing. It is an independent
two-point lattice: meet ORs the restriction and dual complements it. Copies,
conversions, Stores, and Phis preserve it; a merge is incomplete if any live
input is incomplete. A `^` formal stays incomplete even when its known callers
are complete. The receiver's private memory identifies the own-self exception;
another incomplete pointer of the same class has no such permission.

A small `ConstructNode` wraps each source `new`. Its runtime value is the
allocator result; its type adds the OR of that call's actual argument qualifiers.
This distinguishes `new Child(self)` from `new Child(null)` even when both call
the same constructor. Looking only at the shared callee Return would lose this
distinction. The node disappears after TypeCheck, before lowering.

A completed receiver gives a completed view of its pointer fields. Construction
may retain incomplete references but cannot publish them, so reaching the root
`new` boundary with complete arguments makes the entire reachable cycle usable.
There is no group identity or traversal to mark members complete. Conversely,
object-reference fields read through incomplete self remain conservative, even
if the particular stored reference was complete.

The existing optimistic SCCP solve also handles qualifier cycles. For
`p = null; while (n--) p = new Link(p);`, no input introduces incompleteness,
so `p` stays complete and a subsequent field read is allowed. A backedge carrying
self instead makes the Phi and its uses incomplete.

`Construction` selects private self memory during parsing; it retains no
source-use lists or flow facts and performs no recursive Load/Phi/Call query. The old
root and allocation bitsets are gone. There are no field-requirement sets,
hidden `^` memory arguments, or runtime group objects.

`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 `^` 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.