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
|
# Codex notes for Simple Chapter 25
This file is intentionally AI-facing. It records durable invariants, debugging
habits, and project-owner preferences that are easy to miss when reading only
the implementation. `WIP-HANDOFF.md` and `parser-simplification-plan.md` retain
the detailed history; portions of their old branch/status reports are stale
after the Chapter 25 squash.
## Collaboration preferences
- Cliff often debugs the same failure in parallel. If asked to work in a side
repository, update `tmp/` and do not edit the live files he is debugging.
- Stop and discuss before changing a central lattice, constructor, memory, or
call-graph invariant if the required fix starts cascading. Reduced tests and
concrete node-number traces are preferred over speculative broad rewrites.
- Printers are valid debugging tools. If printing changes behavior, fixing the
printer/accessor side effect is immediately high priority.
- Preserve unrelated dirty changes. Emacs lock/backup files are common and are
not permission to clean the tree.
- Do not push without explicit approval. Prefer commits at meaningful test
frontiers with messages describing the architectural change.
## Central parser rule
Simple parses directly to SSA without an AST. Forward declarations mean types
can be incomplete during parsing. The governing rule is:
> Syntax and resolved lexical binding choose SSA topology. Types may sharpen,
> optimize, and reject the graph, but provisional types must not choose its
> shape.
Consequences:
- `compute()` must accept the weakest legal inputs and remain monotonic.
- Parser-only lower bounds such as the old Load/Phi `_con` fields are suspect.
- Calls keep syntactic shape. `ptr.field(args)` supplies `ptr` in the hidden
self slot even before the eventual function kind is known.
- Numeric operations carry an unresolved mode until graph evidence settles
integer versus floating behavior.
- A Load flow type is not guaranteed during parsing. Static declaration facts
may instead be recovered from symbolic field offsets or lexical `Var` state.
## Constructors
- A user constructor is declared `new N = { args -> body }` and invoked as
`new N(args)`.
- Allocation first calls the hidden private `<init>`, then the user constructor.
- Constructor hidden arguments are public memory at index 1, self at index 2,
private self-memory at index 3, followed by user arguments.
- Constructors return private self-memory. `EscapeNode`s publish it into public
memory after construction.
- The hidden initializer supplies defaults/poison. User constructors must set
non-defaultable fields on every exit before the object escapes.
- Constructor field state is tracked in parser `Var` metadata. Early reads of
possibly-uninitialized fields are errors.
- Recursive constructors may reasonably be rejected; constructor chaining must
preserve private-memory initialization state.
## Memory and alias invariants
- The parser carries one ordinary bulk-memory value. Precise alias partitioning
belongs to the graph and optimizer, not nested parser Scope state.
- Alias `#1` is bulk/unresolved memory. A precise alias is `#N`, with `N != 1`.
- Precise memory shapes always use `MemPhiNode`; bulk memory slices use
`BulkMemPhiNode`. Plain `PhiNode`s do not represent memory. If a memory bug
seems to require generic Phi memory state, suspect a representation/worklist
bug instead.
- `MemMerge` inputs are disjoint slices whose union covers all memory. Its
default input means all aliases not explicitly split out.
- `BulkMemPhi` carries `All - exclusions`; parallel `MemPhiNode`s carry the
aliases in the exclusion set.
- A precise consumer may select `MemMerge.alias(N)`. A bulk consumer may not
silently replace a `MemMerge` with `alias(1)` because that discards every
precise side effect.
- In particular, Store-after-MemMerge bypass is valid only when the Store
already has a precise alias. This is covered by Chapter25Test's chained
`grow(1).buf[len++]` regression.
- When an unresolved Store sharpens, rebuild a precise Store plus `MemMerge`;
do not mutate a whole-memory Store in place after users have treated it as
the complete memory state.
- Stores retain control when it represents conditional execution. Removing
the control from the Store after an early return can make the write execute
unconditionally.
- Store width is semantic state (1/2/4/8 bytes, with 0 unresolved) and comes
from the target field declaration, never from the stored value's GLB or the
removed parser constraint.
## Types and monotonicity
- The type lattice must remain complete, symmetric, and bounded. Run TypeTest
after changing Type/Field equality, hashing, duality, meet/join, interning,
serialization, or gather sets.
- `TypeScalar` contains the scalar values ordinary Loads and Stores handle:
integers, floats, memory pointers, and function pointers. Scalar TOP/BOT sit
inside global TOP/BOT and avoid interpreting global BOT as all functions.
- `TypeStruct._open` means fields may still be discovered. `_fref` means the
named structure has been referenced but never authoritatively defined. A
real definition meets away `_fref`; field discovery alone does not.
- Moving an unresolved pointer or checking it for null is valid. Operations
requiring layout or fields must eventually diagnose a never-defined struct.
- `TypeNil` owns pointer nullability. Required pointer fields begin nullable as
an initialization poison and must become their declared non-null type.
- One-step idealization must preserve the type knowledge already established.
`IterPeeps.progressOnList` and `Opto.worklistCheck` are invariant checks, not
assertions to weaken.
- Worklist order must not affect semantics. Rotate deterministic seeds to
expose missing dependencies; never fix a bug by merely favoring one order.
## Functions and escape analysis
- A constant Simple function address must be a `FunPtrNode`, not an ordinary
`ConstantNode`, so the pointer retains an edge to the function Return.
- Before Opto, a live FunPtr keeps its function callable even if no Call is
currently linked. After Opto, an orphan FunPtr may remain as a null/equality
sentinel while the function body becomes dead; this deserves careful error
checking rather than a pre-Opto deletion.
- Extern C functions have no Simple FunNode/Return and are handled only while
linking.
- Unknown-caller Start edges and post-Opto compilation-unit ownership are
separate concepts. Do not overload Start edges merely to keep surviving
functions attached to a compilation unit.
- A global escape fallback means all public functions, not all functions.
Public `<clinit>` functions escape when their class and all ancestor names
are public (no direct name component begins with `_`).
- Escape summaries are monotone discovery facts. If a later SCCP state widens
to FULL/public-only, it must not forget precise private bits discovered from
the same Return earlier.
## Serialization and code generation
- Object files carry canonical types plus ideal Simple IR. Every new semantic
type field, node field, tag, alias map, or mode needs balanced serialization,
deserialization, type upgrading, equality, and bijection testing.
- Avoid smart peepholes after Opto. Machine selection briefly has incomplete
graph edges, and GVN already guarantees uniqueness where required.
- `PtrToIntNode` is the explicit non-null pointer-to-integer conversion used for
C FFI calls. User code must handle null before converting.
- x86 REX arguments are `(ModRM.reg, ModRM.r/m, SIB.index)`. Keep that ordering
aligned with `modrm`; reversing it silently selects the wrong extended
registers. The Chapter 25 Bubble Sort failure exposed this in `MulIX86`.
## Debugging workflow
1. Reduce the first semantic or invariant failure before changing architecture.
2. Refer to graph nodes by class and node ID, e.g. `Store#107`.
3. Find the first phase or IterPeeps count where the graph becomes wrong.
4. For memory bugs, verify both exactly-once alias coverage and that a precise
consumer never requests an alias excluded from bulk without a parallel slice.
5. Distinguish ideal-graph correctness from selection, scheduling, register
allocation, and byte encoding. Compare IR, `CodeGen.asm()`, and `objdump`.
6. Rebuild `sys.o` after compiler changes that affect serialized IR or native
code; stale system objects can make results appear inconsistent.
Useful test ladder:
```text
focused test
chapter test
make tests_raw0
make tests_raw1
make tests_alone
TypeTest after lattice changes
serialize/deserialization bijection after persisted graph changes
```
As of the Chapter 25 squash preparation, `make tests_raw0` passes 360 tests and
Chapter25Test passes its system, Hello World, and Bubble Sort coverage with a
freshly rebuilt `sys.o`.
|