Skip to content
← Back

src/syntax/process/narrow_env.ghul

1
namespace Syntax.Process is
2
use Semantic.Types.Type;
3
use Semantic.Types.INTERSECTION;
4
use Symbol = Semantic.Symbols.Symbol;
5
6
// The flow-analysis facts in force at one program point: the
7
// local variables observed at a type narrower than their
8
// declaration, the local variables known to be definitely
9
// assigned, the local variables known to hold a value, and a
10
// `bottom` marker for points reached only via a diverging path
11
// (return / throw / break / continue).
12
//
13
// The flow pass threads a NARROW_ENV forward through a method,
14
// copies it at branch points, and merges divergent copies with
15
// `join`. A variable absent from `_narrows` is at its declared
16
// type; a variable absent from `_assigned` is not known to be
17
// definitely assigned; a variable absent from `_non_null` is not
18
// known to hold a value. See `docs/claude/flow-sensitive-narrowing.md`.
19
class NARROW_ENV is
20
_narrows: Collections.MAP[Symbol, Type];
21
22
// Locals definitely assigned on every path reaching this
23
// point — the definite-assignment domain.
24
_assigned: Collections.SET[Symbol];
25
26
// Locals known to hold a value at this point — non-null for a
27
// reference type, has-a-value for a NULLABLE[T] (`T?`) value
28
// type. The presence domain; a possible-null-dereference
29
// check (a later phase) consumes it.
30
_non_null: Collections.SET[Symbol];
31
32
// Member-access paths (`receiver.prop`, `receiver.a.b`) known
33
// to hold a value at this point — the same presence domain
34
// lifted from single symbols to re-readable paths. A path
35
// here is narrowed optional -> non-optional at its use sites,
36
// so `if x.y? then x.y.z fi` type-checks. Every hop is a
37
// field or a store-free getter; the flow transfers drop paths
38
// the moment the heap may have changed what they read. See
39
// ACCESS_PATH.
40
_non_null_paths: Collections.SET[ACCESS_PATH];
41
42
// Member-access paths narrowed to a specific type at this
43
// point — the type domain lifted from single symbols to
44
// re-readable paths. A path here is loaded at the narrower
45
// type at its use sites, so `if isa Cat(x.y) then x.y.meow()
46
// fi` type-checks. Composed via INTERSECTION like `_narrows`,
47
// and dropped by the same heap transfers as `_non_null_paths`.
48
_path_narrows: Collections.MAP[ACCESS_PATH, Type];
49
50
// True for an unreachable program point. A bottom env is the
51
// identity element of `join` and carries no facts.
52
is_bottom: bool public;
53
54
init() is
55
_narrows = Collections.MAP[Symbol, Type]();
56
_assigned = Collections.SET[Symbol]();
57
_non_null = Collections.SET[Symbol]();
58
_non_null_paths = Collections.SET[ACCESS_PATH]();
59
_path_narrows = Collections.MAP[ACCESS_PATH, Type]();
60
si
61
62
// The unreachable environment.
63
bottom() -> NARROW_ENV static is
64
let e = NARROW_ENV();
65
e.is_bottom = true;
66
return e;
67
si
68
69
is_empty: bool => !is_bottom /\ _narrows.count == 0;
70
count: int => _narrows.count;
71
72
variables: Collections.Iterable[Symbol] => _narrows.keys;
73
74
contains(v: Symbol) -> bool => _narrows.contains_key(v);
75
76
// The narrowed type recorded for `v`, or null when `v` is
77
// not narrowed in this environment.
78
narrowed_type_of(v: Symbol) -> Type? is
79
let result: Type mut;
80
81
if _narrows.try_get_value(v, result ref) then
82
return result;
83
fi
84
85
return null;
86
si
87
88
// Record `v` as narrowed to `t` (mutating). A bottom env
89
// ignores narrows — it stays unreachable.
90
//
91
// If `v` already carries a narrow `existing`, compose:
92
// `INTERSECTION.try_create(existing, t)`. The factory:
93
// - returns `t` when t strict-subtypes existing (refining),
94
// - returns `existing` when existing strict-subtypes t (the
95
// new narrow is weaker, no-op),
96
// - returns a multi-member intersection when both are
97
// real new facts (e.g., a class plus a sibling trait),
98
// - returns null when the two carry unrelated concrete
99
// identities — no runtime value can be both, so the edge
100
// recording the narrow is statically impossible. The code
101
// on that edge is written against the newly tested type
102
// (`elif isa TUPLE(x) then x.elements`), so `t` replaces
103
// the stale narrow: on an unreachable edge any view is
104
// sound, and the tested type is the one that typechecks.
105
// So stacked narrows like `if isa A(x) then if isa B(x)`
106
// produce `Declared & A & B` rather than dropping A.
107
//
108
// Skipped when composing with an existing narrow doesn't
109
// change the recorded type — every extra fact costs
110
// downstream copies, joins and kill iterations for no
111
// observable effect. A fresh record (no existing narrow) is
112
// always kept; if it turns out to no-op against v's declared
113
// type, the branch-entry `_apply_one` catches it.
114
set_narrow(v: Symbol, t: Type) is
115
if is_bottom then
116
return;
117
fi
118
119
let existing: Type mut;
120
121
if _narrows.try_get_value(v, existing ref) then
122
let composed = INTERSECTION.try_create(existing, t);
123
124
if !composed? then
125
_narrows[v] = t;
126
return;
127
fi
128
129
if composed.matches(existing) then
130
return;
131
fi
132
133
_narrows[v] = composed;
134
else
135
_narrows[v] = t;
136
fi
137
si
138
139
// Record `t` as the narrow for `v`, discarding any existing
140
// narrow instead of composing with it. For callers that have
141
// already computed the absolute narrowed type — the else-edge
142
// complement is derived from the current narrow, so it fully
143
// supersedes it. Composition would be wrong when the complement
144
// collapses to a supertype of the prior narrow (a concrete root
145
// that outlives every eliminated subclass): INTERSECTION would
146
// reinstate the wider prior narrow and the eliminated subclass
147
// would survive.
148
replace_narrow(v: Symbol, t: Type) is
149
if is_bottom then
150
return;
151
fi
152
153
_narrows[v] = t;
154
si
155
156
// Path-keyed mirror of `replace_narrow`.
157
replace_path_narrow(path: ACCESS_PATH, t: Type) is
158
if is_bottom then
159
return;
160
fi
161
162
_path_narrows[path] = t;
163
si
164
165
// Forget any narrow for `v` (mutating) — used by the
166
// assignment transfer function.
167
drop_narrow(v: Symbol) is
168
if _narrows.contains_key(v) then
169
_narrows.remove(v);
170
fi
171
si
172
173
// The locals definitely assigned at this point.
174
assigned_variables: Collections.Iterable[Symbol] => _assigned;
175
176
// True iff `v` is definitely assigned at this point.
177
is_assigned(v: Symbol) -> bool => _assigned.contains(v);
178
179
// Record `v` as definitely assigned (mutating). A bottom env
180
// ignores it — it stays unreachable.
181
set_assigned(v: Symbol) is
182
if is_bottom then
183
return;
184
fi
185
186
_assigned.add(v);
187
si
188
189
// The locals known to hold a value at this point.
190
non_null_variables: Collections.Iterable[Symbol] => _non_null;
191
192
// True iff `v` is known to hold a value at this point.
193
is_non_null(v: Symbol) -> bool => _non_null.contains(v);
194
195
// Record `v` as known to hold a value (mutating). A bottom
196
// env ignores it — it stays unreachable.
197
//
198
// Callers should filter out presence facts on non-optional
199
// targets themselves: NARROW_ENV can't safely check `v.type`
200
// here because the symbol's live type reflects whatever the
201
// outer walk state applied, not the fresh env being built —
202
// a redundant-looking fact recorded on a nested if body may
203
// still be observable at the branch entry when the outer
204
// narrow is restored.
205
set_non_null(v: Symbol) is
206
if is_bottom then
207
return;
208
fi
209
210
_non_null.add(v);
211
si
212
213
// Forget that `v` is known to hold a value (mutating) — used
214
// by the assignment transfer function.
215
drop_non_null(v: Symbol) is
216
if _non_null.contains(v) then
217
_non_null.remove(v);
218
fi
219
si
220
221
// The member-access paths known to hold a value at this point.
222
non_null_paths: Collections.Iterable[ACCESS_PATH] => _non_null_paths;
223
224
// True iff `path` is known to hold a value at this point.
225
is_non_null_path(path: ACCESS_PATH) -> bool => _non_null_paths.contains(path);
226
227
// Record `path` as known to hold a value (mutating). A bottom
228
// env ignores it — it stays unreachable.
229
set_non_null_path(path: ACCESS_PATH) is
230
if is_bottom then
231
return;
232
fi
233
234
_non_null_paths.add(path);
235
si
236
237
// The member-access paths carrying a type narrow at this point.
238
narrowed_paths: Collections.Iterable[ACCESS_PATH] => _path_narrows.keys;
239
240
// The narrowed type recorded for `path`, or null when it is
241
// not narrowed in this environment.
242
narrowed_type_of_path(path: ACCESS_PATH) -> Type? is
243
let result: Type mut;
244
245
if _path_narrows.try_get_value(path, result ref) then
246
return result;
247
fi
248
249
return null;
250
si
251
252
// Record `path` as narrowed to `t` (mutating). Bottom env
253
// ignores it. Stacks like `set_narrow` — a prior narrow on
254
// the same path composes with `t` via INTERSECTION, and an
255
// unrelated-concretes pair (statically-impossible test edge)
256
// replaces the stale narrow with `t` for the same reason.
257
set_path_narrow(path: ACCESS_PATH, t: Type) is
258
if is_bottom then
259
return;
260
fi
261
262
let existing: Type mut;
263
264
if _path_narrows.try_get_value(path, existing ref) then
265
let composed = INTERSECTION.try_create(existing, t);
266
267
_path_narrows[path] = if composed? then composed else t fi;
268
else
269
_path_narrows[path] = t;
270
fi
271
si
272
273
// Forget every tracked path — the call transfer: a
274
// possibly-storing callee may have changed anything a path
275
// reads. Same reasoning for path type narrows.
276
drop_all_paths() is
277
_non_null_paths.clear();
278
_path_narrows.clear();
279
si
280
281
// Forget every path that reads through a property getter —
282
// the heap-store transfer: a store anywhere can change what a
283
// getter returns. Fields-only paths survive; a store to a
284
// field they read through is handled by drop_paths_through.
285
// Applies to both presence and type-narrow slots.
286
drop_getter_paths() is
287
let doomed = Collections.LIST[ACCESS_PATH]();
288
289
for p in _non_null_paths do
290
if p.has_getter_hop then
291
doomed.add(p);
292
fi
293
od
294
295
for p in doomed do
296
_non_null_paths.remove(p);
297
od
298
299
let doomed_narrows = Collections.LIST[ACCESS_PATH]();
300
301
for p in _path_narrows.keys do
302
if p.has_getter_hop then
303
doomed_narrows.add(p);
304
fi
305
od
306
307
for p in doomed_narrows do
308
_path_narrows.remove(p);
309
od
310
si
311
312
// Forget every path that reads through `member` at any hop —
313
// the member-store transfer. Keyed on the member symbol, not
314
// the written receiver, so a store through an aliased
315
// receiver still invalidates. Applies to both presence and
316
// type-narrow slots.
317
drop_paths_through(member: Symbol) is
318
let doomed = Collections.LIST[ACCESS_PATH]();
319
320
for p in _non_null_paths do
321
if p.contains_member(member) then
322
doomed.add(p);
323
fi
324
od
325
326
for p in doomed do
327
_non_null_paths.remove(p);
328
od
329
330
let doomed_narrows = Collections.LIST[ACCESS_PATH]();
331
332
for p in _path_narrows.keys do
333
if p.contains_member(member) then
334
doomed_narrows.add(p);
335
fi
336
od
337
338
for p in doomed_narrows do
339
_path_narrows.remove(p);
340
od
341
si
342
343
// Forget every tracked path rooted at `root` — used when the
344
// root is reassigned, which redirects every `root.…` path.
345
// Applies to both presence and type-narrow slots.
346
drop_paths_rooted_at(root: Symbol) is
347
let doomed = Collections.LIST[ACCESS_PATH]();
348
349
for p in _non_null_paths do
350
if p.root == root then
351
doomed.add(p);
352
fi
353
od
354
355
for p in doomed do
356
_non_null_paths.remove(p);
357
od
358
359
let doomed_narrows = Collections.LIST[ACCESS_PATH]();
360
361
for p in _path_narrows.keys do
362
if p.root == root then
363
doomed_narrows.add(p);
364
fi
365
od
366
367
for p in doomed_narrows do
368
_path_narrows.remove(p);
369
od
370
si
371
372
// Forget every fact a heap mutation can invalidate: narrows
373
// and presence facts keyed on fields or properties, and every
374
// member-access path. Local-variable facts survive — a callee
375
// cannot reach a local. Used by controlled walks whose branch
376
// environments derive from a snapshot taken before the walk:
377
// when the walk killed heap facts, the snapshot's heap facts
378
// cannot be trusted on the edges built from it.
379
drop_heap_facts() is
380
let stale = Collections.LIST[Symbol]();
381
382
for v in _narrows.keys do
383
if isa Semantic.Symbols.Field(v) \/ isa Semantic.Symbols.Property(v) then
384
stale.add(v);
385
fi
386
od
387
388
for v in stale do
389
_narrows.remove(v);
390
od
391
392
let stale_presence = Collections.LIST[Symbol]();
393
394
for v in _non_null do
395
if isa Semantic.Symbols.Field(v) \/ isa Semantic.Symbols.Property(v) then
396
stale_presence.add(v);
397
fi
398
od
399
400
for v in stale_presence do
401
_non_null.remove(v);
402
od
403
404
_non_null_paths.clear();
405
_path_narrows.clear();
406
si
407
408
copy() -> NARROW_ENV is
409
let e = NARROW_ENV();
410
e.is_bottom = is_bottom;
411
412
for v in _narrows.keys do
413
e._narrows[v] = _narrows[v];
414
od
415
416
for v in _assigned do
417
e._assigned.add(v);
418
od
419
420
for v in _non_null do
421
e._non_null.add(v);
422
od
423
424
for p in _non_null_paths do
425
e._non_null_paths.add(p);
426
od
427
428
for p in _path_narrows.keys do
429
e._path_narrows[p] = _path_narrows[p];
430
od
431
432
return e;
433
si
434
435
// A copy carrying only the definite-assignment facts — no
436
// narrows, reachable. Used where the narrowing facts must be
437
// discarded conservatively (after a try statement) but
438
// assignments made before it still hold.
439
with_only_assigned() -> NARROW_ENV is
440
let e = NARROW_ENV();
441
442
for v in _assigned do
443
e._assigned.add(v);
444
od
445
446
return e;
447
si
448
449
// Merge two environments at a control-flow merge point.
450
// `bottom` is the identity (a diverging branch contributes
451
// nothing). Every domain merges by intersection: a variable
452
// survives the narrowing merge only when narrowed in BOTH
453
// inputs (merged type = least upper bound); a variable is
454
// definitely assigned after the merge only when assigned in
455
// BOTH inputs; likewise known-non-null only when in both.
456
join(a: NARROW_ENV, b: NARROW_ENV) -> NARROW_ENV static is
457
if a.is_bottom then
458
return b.copy();
459
fi
460
461
if b.is_bottom then
462
return a.copy();
463
fi
464
465
let result = NARROW_ENV();
466
467
for v in a.variables do
468
if let ta = a.narrowed_type_of(v), tb = b.narrowed_type_of(v) then
469
// Two narrows over the same closed root join by
470
// subtype-set union — exact, and it keeps the
471
// optional flag when either edge can be null.
472
// The general LUB is the fallback for every
473
// other shape.
474
let merged mut = Semantic.Types.ONE_OF.try_join(ta, tb);
475
476
if !merged? then
477
let lub = Semantic.LEAST_UPPER_BOUND_MAP();
478
lub.add(ta);
479
lub.add(tb);
480
481
merged = lub.get_result();
482
fi
483
484
if merged? then
485
result.set_narrow(v, merged);
486
fi
487
fi
488
od
489
490
for v in a.assigned_variables do
491
if b.is_assigned(v) then
492
result.set_assigned(v);
493
fi
494
od
495
496
for v in a.non_null_variables do
497
if b.is_non_null(v) then
498
result.set_non_null(v);
499
fi
500
od
501
502
for p in a.non_null_paths do
503
if b.is_non_null_path(p) then
504
result.set_non_null_path(p);
505
fi
506
od
507
508
for p in a.narrowed_paths do
509
if let ta = a.narrowed_type_of_path(p), tb = b.narrowed_type_of_path(p) then
510
// Two narrows over the same closed root join by
511
// subtype-set union — mirrors the symbol case.
512
let merged mut = Semantic.Types.ONE_OF.try_join(ta, tb);
513
514
if !merged? then
515
let lub = Semantic.LEAST_UPPER_BOUND_MAP();
516
lub.add(ta);
517
lub.add(tb);
518
519
merged = lub.get_result();
520
fi
521
522
if merged? then
523
result.set_path_narrow(p, merged);
524
fi
525
fi
526
od
527
528
return result;
529
si
530
si
531
si