summaryrefslogtreecommitdiff
path: root/chapter25/parser-simplification-plan.md
blob: 986643ea409faac53397a3e62a0057ff44090edd (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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# Direct-to-SSA Parser Simplification Plan

## Purpose

The constructor rewrite exposed a deeper problem: the one-pass parser sometimes
uses provisional type information to choose SSA graph structure.  Forward
references make that information incomplete and parse-order dependent.

The design rule for this work is:

> Syntax and resolved lexical bindings determine graph shape.  Types refine and
> validate the graph, but provisional types do not decide its shape.

This is an experiment, not an irreversible cleanup.  We need to be able to
abandon the simplification completely, or restore individual mechanisms that
turn out to have been necessary.

## Preserve the old design before removing it

Do not preserve old code as large commented-out blocks.  Git is the archive;
the working implementation should remain small enough to understand.

Before implementation begins:

1. Confirm the worktree is clean and record the current failing-test baseline.
2. Create a named, permanent anchor at the last commit before simplification,
   for example an annotated tag `ch25-parser-before-simplification`.
3. Do the experiment on a dedicated branch, for example
   `ch25-parser-simplification`.
4. Push the anchor and branch if a remote backup is desired before large
   deletions.
5. Never squash the experimental commits until the design has proved itself.

The anchor is the complete "this was a giant mistake" escape hatch.  Switching
back to a branch made from that anchor restores the old implementation exactly.
No reset of the experimental branch is required.

Removed mechanisms must each have their own small commit.  A commit should
remove one idea, not mix its deletion with several replacements.  Suggested
commit sequence:

1. Add characterization tests and inventories.
2. Remove one old constraint mechanism.
3. Make weak-input computation total.
4. Add the smallest replacement demanded by a failing test.

This history supports several levels of recovery:

- Recover the entire old design by creating or switching to a branch at the
  preservation tag.
- Restore a complete deleted file from the tag with `git restore --source`.
- Restore selected hunks interactively with `git restore -p --source`.
- Reintroduce a logically complete mechanism by reverting its deletion commit
  or cherry-picking a later restoration commit.
- Compare the experiment with the old implementation at any time using the
  preservation tag as the diff base.

Avoid using a stash as the archive: stashes are unnamed working state, are easy
to drop, and do not provide the reviewable sequence needed for selective
restoration.

When an old mechanism is restored, bring it back in a new, named commit and
record the test that required it.  Do not silently copy code out of history.
That preserves evidence about which complexity is essential.

## Working discipline

Work through `tests_raw0` chapter by chapter.  Keep all earlier chapters passing
before advancing.  The expected difficulty landmarks are Chapter 10, where
functions are exercised seriously, and Chapter 13, where recursive forward
references and cyclic types become prominent.

Use the following test ladder:

1. The single test or test class driving the current change.
2. The current chapter.
3. All earlier raw0 chapters.
4. `tests_raw0` when a milestone is reached.
5. `TypeTest` after every change to `Type`, `Field`, lattice operations,
   interning inputs, equality, hashing, or duality.

Golden-string mismatches are not a gate during structural work unless the
string reveals a semantic or graph-shape regression.  Record and defer expected
text churn rather than continually rewriting goldens.

Treat these as compiler invariant failures, not ordinary test failures:

- `compute()` must be monotonic, including when inputs are weak or unresolved.
- `Opto.worklistCheck` failures require finding the non-monotonic computation;
  they are not assertions to weaken.
- IterPeeps' `Non-monotonic peep` check applies the same rule to iterative
  transformations.
- `ideal()` runs only during iterative optimization, not Opto, but its
  replacement must preserve the type knowledge already established.

Commit whenever the test frontier advances or one architectural mechanism is
removed.  Each commit message should state the highest passing chapter and
whether `TypeTest` was run.

## Phase 0: Baseline and inventory

1. Fix the local `_Scan` constructor migration so it does not obscure parser
   work.
2. Record the current raw0 failures by chapter and test name.
3. Inventory parser-written constraints and type-sharpening side channels:
   `_con` fields, mutable signatures, parser mutations of `TYPES`, forced memory
   types, and assertions that assume already-sharp inputs.
4. For every candidate deletion, record its writers, readers, and the first
   test that appears to need it.
5. Add small characterization tests when behavior is important but undocumented.

The inventory is a deletion checklist, not a commitment to replace every item.
Code that no passing test requires stays deleted.

## Phase 1: Remove parser-supplied node constraints

Start with `_con`-style fields in `PhiNode`, `LoadNode`, `StoreNode`, and related
nodes.

For each node kind:

1. Delete the parser writer and the node field in a narrow commit.
2. Make `compute()` accept the weakest legal inputs.  A `Type.BOTTOM` input must
   produce a conservative weak output instead of triggering a cast or sharpness
   assertion.
3. Separate structural slot invariants from type-sharpness assumptions.  It is
   valid to know that an input position is the memory edge without knowing a
   sharp `TypeMem` for it.
4. Restore only the structure that a failing test proves necessary.

If an ordinary Phi cannot preserve the structural fact that it merges memory,
introduce a `MemPhiNode` or an explicit immutable Phi kind.  It may lift weak
inputs to the weakest memory type.  It must not contain a parser-chosen sharp
lower bound disguised as another `_con`.

### Keep arithmetic flavor out of the parser

Arithmetic syntax determines the operation (`+`, `-`, `*`, `/`, unary `-`),
but not necessarily its integer or floating-point machine flavor.  The current
`Node.widen()` is a parser-time semantic rewrite: it inspects the operands'
current types, replaces integer nodes with FP nodes, and inserts `ToFloatNode`s.
A weak forward-reference input can therefore permanently choose the wrong SSA
shape.

Replace this decision with a generic or unresolved arithmetic node, or make the
syntax-level arithmetic node itself generic until its operand family resolves.
It must:

- compute a weak result while either operand family is unknown;
- resolve monotonically to integer or floating-point arithmetic when sufficient
  information arrives;
- insert integer-to-float conversions only after the FP choice is definitive;
- report mixed or invalid operands during type checking rather than guessing
  during parsing; and
- be completely resolved before instruction selection.

Compound assignments and increment/decrement use the same mechanism; they must
not retain a separate parser-time `widen()` decision path.

## Phase 2: Separate name binding from type inference

Locals remain define-before-use.  The unresolved case is an unqualified field
name used inside a function before a later instance or class field declaration
determines its owner.

Introduce unresolved SSA operations rather than guessing from provisional
types:

- `UnresolvedLoad` records the identifier, source position, definitive lexical
  scope object, ordered possible base pointers, and bulk memory input.
- `UnresolvedStore` records the same binding information plus its value and the
  constructor context needed to decide initialization legality later.
- Closing the relevant scope resolves all pending operations by lexical order.
- Resolution selects the base pointer and alias and rewrites the node to the
  ordinary resolved operation.

Do not use pointer-type sharpening to choose ordinary lexical bindings.  Types
may reject a resolved operation, but the closed scope is authoritative about
which declaration the name denotes.

The `init` bit on a resolved Store is decided at this same point.  It is true
only when the winning declaration belongs to the receiver being initialized by
the current constructor.  Merely being textually inside some constructor is not
enough.

## Phase 3: Recover alias precision after unresolved stores

Before binding, an unknown store uses bulk alias `#1` and conservatively updates
bulk memory.  Preserve enough information in `UnresolvedStore` to recover the
pre-store alias bundle when its target becomes known.

After resolution, Opto may transform a Phi of memory merges into a merge of
per-alias Phis:

```text
Phi(MemMerge(entry...), MemMerge(back...))
    =>
MemMerge(aliasA = Phi(entryA, backA),
         aliasB = Phi(entryB, backB), ...)
```

For a resolved store to alias A, alias A's loop backedge passes through the
store; unrelated aliases bypass it.

Requirements for this idealization:

- Handle missing slices through the MemMerge default/bulk input.
- Create alias Phis on demand; aliases need not all be known at loop creation.
- Memoize a replacement loop Phi before recursively processing its backedge.
- Use only the canonical direction shown above to prevent transform oscillation.
- Keep unresolved memory operations resistant to destructive idealization until
  their owning scope closes.

Pre-splitting memory during parsing remains an eager optimization when aliases
are already known.  This transform is the lazy equivalent when they are not.

## Phase 4: Keep call shape syntactic

Call SSA shape must not depend on whether a provisional function type currently
looks like an instance function.

The syntax `ptr.fld(args)` always supplies `ptr` in the hidden self slot:

```text
call(load(ptr, fld), self=ptr, args...)
```

This remains true if the field later resolves to a stored top-level function;
that function ignores the universal self argument.  An ordinary call through a
separately loaded function value uses the normal non-instance/default self.

Function types validate calls after construction.  They do not change call
arity or add/remove SSA inputs after resolution.

## Phase 5: Split declared function contracts from inferred signatures

Audit `FunNode._sig` after the earlier constraints are removed.

Function arguments have authoritative declared syntax, while returns are graph
derived.  A declared forward type such as `B` has stable symbolic identity but
only a provisional current realization.

Represent the external/default caller with a non-constant source node tied to
the declared type binding:

```text
UnknownCaller(B-binding)
```

Its computed runtime value type begins as weak open `B{...}` and monotonically
sharpens when the binding resolves.  Updating the binding schedules its users.
Private functions with completely known call sites may omit this default caller
and sharpen their parameters from actual arguments.  Escaping functions retain
it as their worst-case caller.

Classify every use of `_sig` as one of:

- immutable declared argument contract;
- cached graph-derived information;
- unresolved symbolic binding.

Delete cached derived state.  Keep authoritative declarations explicitly.
Derive return information from Fun/Parm/Return graph edges.  Recursive functions
must converge through weak-input-tolerant `compute()` rather than parser mutation
of a parallel signature cache.

## Phase 6: Make field finality a four-state type fact

Use two bits in `Field`, with accessors hiding the representation:

```text
00 unknown
01 declared mutable
10 declared final
11 conflicting finality (BAD_FINAL)
```

The discovery-combination operation combines evidence; with this encoding it
can normally be bitwise OR.  `UNKNOWN` is the identity and `BAD_FINAL` is
absorbing.  Confirm the operation matches the repository's meet/join lattice
orientation.

Add the bits to equality, hashing, duality, printing, and all Field construction
paths.  A weak forward reference carries `UNKNOWN`, never guessed mutable.
Conflicting authoritative declarations produce `BAD_FINAL`; computation remains
total and type checking later reports the program error.

Add representative Fields for all four states to TypeTest's `gather()` universe.
Verify pairwise operations plus commutativity, associativity, idempotence, duals,
hashing, and interning behavior.  Cyclic handling and interning should fall out
of the existing machinery, but TypeTest must demonstrate that they do.

Final-store validation happens only after binding and type closure:

- final and initializing store: legal;
- final and non-initializing store: error;
- mutable field: initialization marker is irrelevant;
- bad finality: declaration/type error.

## Phase 7: Preserve cyclic type closure

Keep `Type.closeOver()` responsible for tying recursive named types together.
For mutually recursive A/B structures it must collect partial versions, merge
same-name fields (including four-state finality), remove the open marker when
the declaration is complete, and make recursive occurrences reference the same
canonical Java object.

Do not make SSA construction depend on whether `closeOver()` has already run.
Closing types sharpens existing nodes and wakes their users; it does not rebuild
parser-chosen graph topology.

Chapter 13 is the first major acceptance gate for this phase.

## Phase 8: Advance the chapter frontier

For each failing chapter:

1. Reduce the first semantic or invariant failure to the smallest useful test.
2. Decide whether it demonstrates missing required structure or merely an old
   golden string.
3. Add back only the smallest mechanism justified by the test.
4. Run the chapter, all earlier chapters, and TypeTest when applicable.
5. Commit the new frontier before moving on.

Expected order of pressure:

- Chapters 1-9: establish that weak-input computation did not disturb basics.
- Chapter 10: functions, calls, and parameter computation.
- Chapter 13: recursive forward functions and cyclic structures.
- Later chapters: fields, finality, memory alias recovery, escaping functions,
  and separate compilation interactions.

Do not begin by restoring later-chapter machinery preemptively.

## Decision gates and rollback criteria

Pause and compare against the preservation anchor when any of these occurs:

- A replacement requires more state or special cases than the mechanism removed.
- Parser graph shape still depends on provisional types.
- Alias recovery requires global reconstruction with no monotonic canonical form.
- Recursive type or function convergence requires non-monotonic mutation.
- Earlier chapters repeatedly regress as later mechanisms are added.

At a gate, choose explicitly among:

1. Continue with the simplified mechanism.
2. Restore one old component and document the test proving its value.
3. Start a narrower experiment branch from the preservation anchor.
4. Abandon the experiment and resume from the preserved design.

Abandoning the experiment is a supported result, not a failed recovery.  The
tag, unsquashed commits, and narrow deletions guarantee that both the complete
old parser and individual removed mechanisms remain available.

## Completion criteria

The simplification is successful when:

- `tests_raw0`, then `tests_raw1`, and finally the broader suite pass apart from
  deliberately deferred golden updates;
- `TypeTest` passes with all finality states included;
- no parser decision about SSA topology depends on a provisional inferred type;
- unresolved lexical operations resolve deterministically at scope close;
- memory precision lost during unresolved parsing is recovered monotonically;
- calls obtain self from syntax, not inferred callee kind;
- declared function arguments remain authoritative without a parallel mutable
  inferred signature;
- removed code that was never demanded by tests remains absent; and
- the preservation anchor and commit sequence still provide complete and
  selective rollback paths.