Skip to content
← Back

src/syntax/process/narrowing/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
// One call a heap fact survived on the way to a use. Narrowing is
7
// optimistic: a call does not drop heap facts, it is recorded
8
// against every fact live across it, and a use of the fact is
9
// checked against the record — a crossing whose callee provably
10
// left the fact alone is discharged, and relying on a fact with
11
// an undischarged crossing is a compile error at the use site.
12
class CROSSING(location: Source.LOCATION, callee: Semantic.Symbols.Function?)
13
14
// The flow-analysis facts in force at one program point: the
15
// local variables observed at a type narrower than their
16
// declaration, the local variables known to be definitely
17
// assigned, the local variables known to hold a value, and a
18
// `bottom` marker for points reached only via a diverging path
19
// (return / throw / break / continue).
20
//
21
// The flow pass threads a NARROW_ENV forward through a method,
22
// copies it at branch points, and merges divergent copies with
23
// `join`. A variable absent from `_narrows` is at its declared
24
// type; a variable absent from `_assigned` is not known to be
25
// definitely assigned; a variable absent from `_non_null` is not
26
// known to hold a value. See `docs/claude/flow-sensitive-narrowing.md`.
27
class NARROW_ENV is
28
_narrows: Collections.MAP[Symbol, Type]
29
30
// Locals definitely assigned on every path reaching this
31
// point — the definite-assignment domain.
32
_assigned: Collections.SET[Symbol]
33
34
// Methods of the enclosing type called on `self` on every path
35
// reaching this point - the same must-domain as `_assigned`, and
36
// joined the same way. A constructor's field-assignment answer
37
// follows the callees it cannot avoid reaching.
38
_called: Collections.SET[Symbol]
39
40
// Locals known to hold a value at this point — non-null for a
41
// reference type, has-a-value for a NULLABLE[T] (`T?`) value
42
// type. The presence domain; a possible-null-dereference
43
// check (a later phase) consumes it.
44
_non_null: Collections.SET[Symbol]
45
46
// Member-access paths (`receiver.prop`, `receiver.a.b`) known
47
// to hold a value at this point — the same presence domain
48
// lifted from single symbols to re-readable paths. A path
49
// here is narrowed optional -> non-optional at its use sites,
50
// so `if x.y? then x.y.z fi` type-checks. Every hop is a
51
// field or a store-free getter; the flow transfers drop paths
52
// the moment the heap may have changed what they read. See
53
// ACCESS_PATH.
54
_non_null_paths: Collections.SET[ACCESS_PATH]
55
56
// Member-access paths narrowed to a specific type at this
57
// point — the type domain lifted from single symbols to
58
// re-readable paths. A path here is loaded at the narrower
59
// type at its use sites, so `if isa Cat(x.y) then x.y.meow()
60
// fi` type-checks. Composed via INTERSECTION like `_narrows`,
61
// and dropped by the same heap transfers as `_non_null_paths`.
62
_path_narrows: Collections.MAP[ACCESS_PATH, Type]
63
64
// The calls each heap fact has survived since it was last
65
// established, one map per fact domain. A local's facts never
66
// cross - a callee cannot reach a local - so only fields,
67
// properties and paths take entries. Establishing a fact
68
// afresh clears its record; an else-edge complement, which
69
// derives from the prior narrow, inherits it instead.
70
_non_null_crossings: Collections.MAP[Symbol, Collections.LIST[CROSSING]]
71
_narrow_crossings: Collections.MAP[Symbol, Collections.LIST[CROSSING]]
72
_path_presence_crossings: Collections.MAP[ACCESS_PATH, Collections.LIST[CROSSING]]
73
_path_narrow_crossings: Collections.MAP[ACCESS_PATH, Collections.LIST[CROSSING]]
74
75
// The crossing-log index at which each heap fact was
76
// established, one map per fact domain mirroring the crossing
77
// records. Adoption replays a log range onto an environment's
78
// facts; a replayed crossing that ran before a fact's own
79
// establishing test cannot invalidate it, and the index lets
80
// `note_crossing_at` skip it. A fact with no recorded index is
81
// treated as created at the start of the log, so every
82
// replayed crossing attaches — the pre-mechanism behaviour,
83
// and the conservative direction.
84
_non_null_created_at: Collections.MAP[Symbol, int]
85
_narrow_created_at: Collections.MAP[Symbol, int]
86
_path_presence_created_at: Collections.MAP[ACCESS_PATH, int]
87
_path_narrow_created_at: Collections.MAP[ACCESS_PATH, int]
88
89
// The creation index the setters stamp on facts they establish
90
// afresh. The condition analyzer sets it to the log count the
91
// walk recorded at each leaf test before adding that leaf's
92
// facts; the flow sets it to its own log count before creating
93
// a fact mid-walk. Zero attaches everything.
94
creation_mark: int public
95
96
// True for an unreachable program point. A bottom env is the
97
// identity element of `join` and carries no facts.
98
is_bottom: bool public
99
100
init() is
101
_narrows = Collections.MAP[Symbol, Type]()
102
_assigned = Collections.SET[Symbol]()
103
_called = Collections.SET[Symbol]()
104
_non_null = Collections.SET[Symbol]()
105
_non_null_paths = Collections.SET[ACCESS_PATH]()
106
_path_narrows = Collections.MAP[ACCESS_PATH, Type]()
107
_non_null_crossings = Collections.MAP[Symbol, Collections.LIST[CROSSING]]()
108
_narrow_crossings = Collections.MAP[Symbol, Collections.LIST[CROSSING]]()
109
_path_presence_crossings = Collections.MAP[ACCESS_PATH, Collections.LIST[CROSSING]]()
110
_path_narrow_crossings = Collections.MAP[ACCESS_PATH, Collections.LIST[CROSSING]]()
111
_non_null_created_at = Collections.MAP[Symbol, int]()
112
_narrow_created_at = Collections.MAP[Symbol, int]()
113
_path_presence_created_at = Collections.MAP[ACCESS_PATH, int]()
114
_path_narrow_created_at = Collections.MAP[ACCESS_PATH, int]()
115
si
116
117
_is_heap_fact(v: Symbol) -> bool =>
118
isa Semantic.Symbols.Field(v) \/
119
isa Semantic.Symbols.Property(v) \/
120
is_closure_assigned(v)
121
122
// A local a closure body assigns is a heap fact. Facts on
123
// other locals are never recorded as crossing a call, because
124
// a callee cannot reach a caller's stack slot - but such a
125
// local lives in a heap cell shared with the closure, so a
126
// call can rewrite it, and its facts take crossings and are
127
// judged at each leaning use exactly as a field's are.
128
is_closure_assigned(v: Symbol) -> bool static =>
129
if let variable: Semantic.Symbols.Variable = v then
130
variable.is_closure_assigned
131
else
132
false
133
fi
134
135
// Record `crossing` against every heap fact live at this
136
// point, in every domain.
137
// Copy `v`'s crossing records from `other` into this
138
// environment. For an environment rebuilt fact-by-fact from
139
// another (the loop kept-env), which would otherwise carry
140
// the facts stripped of the crossings still owed on them.
141
copy_crossings_of(v: Symbol, other: NARROW_ENV) is
142
let existing: Collections.LIST[CROSSING] mut
143
144
if other._non_null_crossings.try_get_value(v, existing ref) then
145
_non_null_crossings[v] = _copied_crossings(existing)
146
fi
147
148
if other._narrow_crossings.try_get_value(v, existing ref) then
149
_narrow_crossings[v] = _copied_crossings(existing)
150
fi
151
152
let created mut = 0
153
154
if other._non_null_created_at.try_get_value(v, created ref) then
155
_non_null_created_at[v] = created
156
fi
157
158
if other._narrow_created_at.try_get_value(v, created ref) then
159
_narrow_created_at[v] = created
160
fi
161
si
162
163
// The heap facts a crossing would be attached to, narrowed
164
// variables first and presence-only ones after, so a variable
165
// carrying both is described by its narrow. Mirrors the
166
// enumeration `note_crossing` performs.
167
heap_fact_variables: Collections.Iterable[Symbol] is
168
let result = Collections.LIST[Symbol]()
169
170
if is_bottom then
171
return result
172
fi
173
174
for v in _narrows.keys do
175
if _is_heap_fact(v) then
176
result.add(v)
177
fi
178
od
179
180
for v in _non_null do
181
if _is_heap_fact(v) /\ !_narrows.contains_key(v) then
182
result.add(v)
183
fi
184
od
185
186
return result
187
si
188
189
// Path analogue of `heap_fact_variables`.
190
heap_fact_paths: Collections.Iterable[ACCESS_PATH] is
191
let result = Collections.LIST[ACCESS_PATH]()
192
193
if is_bottom then
194
return result
195
fi
196
197
for p in _path_narrows.keys do
198
result.add(p)
199
od
200
201
for p in _non_null_paths do
202
if !_path_narrows.contains_key(p) then
203
result.add(p)
204
fi
205
od
206
207
return result
208
si
209
210
note_crossing(crossing: CROSSING) is
211
if is_bottom then
212
return
213
fi
214
215
for v in _non_null do
216
if _is_heap_fact(v) then
217
_append_crossing(_non_null_crossings, v, crossing)
218
fi
219
od
220
221
for v in _narrows.keys do
222
if _is_heap_fact(v) then
223
_append_crossing(_narrow_crossings, v, crossing)
224
fi
225
od
226
227
for p in _non_null_paths do
228
_append_path_crossing(_path_presence_crossings, p, crossing)
229
od
230
231
for p in _path_narrows.keys do
232
_append_path_crossing(_path_narrow_crossings, p, crossing)
233
od
234
si
235
236
// The formation rule of the combined-solve prototype: a fact
237
// read through a getter whose own re-read the solved relations
238
// cannot discharge is dead at its first use, so it is not
239
// formed. The decline is recorded as a kill at the getter's
240
// own call, so a later solve that backs the getter re-walks
241
// the file and the fact forms. With nothing solved the fact
242
// forms, and its first load records the same question for the
243
// ledger to ask after the solve.
244
_getter_hops(target: Symbol?, path: ACCESS_PATH?) -> Collections.LIST[Semantic.Symbols.Property] static is
245
let result = Collections.LIST[Semantic.Symbols.Property]()
246
247
if let property: Semantic.Symbols.Property = target then
248
result.add(property)
249
fi
250
251
if path? then
252
if let property: Semantic.Symbols.Property = path.root then
253
result.add(property)
254
fi
255
256
for m in path.members do
257
if let property: Semantic.Symbols.Property = m then
258
result.add(property)
259
fi
260
od
261
fi
262
263
return result
264
si
265
266
_declines_getter_fact(target: Symbol?, path: ACCESS_PATH?, is_presence: bool) -> bool static is
267
if !KILL_LEDGER.can_kill_in_walk then
268
// Nothing solved yet, so the fact forms; each getter it
269
// reads through is recorded as a kept decision at the
270
// getter's own call, so the re-ask after the solve
271
// reaches the formation even when no load follows it.
272
if let file_name = KILL_LEDGER.current_file then
273
for getter in _getter_hops(target, path) do
274
if let read = getter.read_function then
275
let kill = KILL(file_name, CROSSING(getter.location, read), target, path, is_presence, false)
276
277
kill.declined = true
278
279
KILL_LEDGER.record(kill)
280
fi
281
od
282
fi
283
284
return false
285
fi
286
287
let getter = RELIANCES.first_unproven_getter(target, path, is_presence)
288
289
if !getter? then
290
return false
291
fi
292
293
if let file_name = KILL_LEDGER.current_file, property: Semantic.Symbols.Property = getter, read = property.read_function then
294
let kill = KILL(file_name, CROSSING(getter.location, read), target, path, is_presence, true)
295
296
kill.declined = true
297
298
KILL_LEDGER.record(kill)
299
fi
300
301
return true
302
si
303
304
// As `note_crossing`, but for a crossing replayed from the log
305
// at `index`: a fact established after the crossing ran cannot
306
// have been invalidated by it — the establishing test observed
307
// the post-call heap — so the crossing skips that fact. Facts
308
// with no recorded creation index take every crossing, the
309
// pre-mechanism behaviour.
310
note_crossing_at(crossing: CROSSING, index: int) is
311
if is_bottom then
312
return
313
fi
314
315
// The replay-path half of the combined-solve prototype's
316
// in-walk kill transfer: a fact the solved relations cannot
317
// back across this call dies here instead of taking the
318
// crossing. The active-environment half lives in
319
// NARROWING_FLOW._apply_relations, which also restores
320
// symbol types; an environment adopted onto is not in
321
// force, so removal alone is enough here.
322
if KILL_LEDGER.can_kill_in_walk then
323
_apply_relations_to_self(crossing, index)
324
fi
325
326
for v in _non_null do
327
if _is_heap_fact(v) /\ _created_by(_non_null_created_at, v, index) then
328
_append_crossing(_non_null_crossings, v, crossing)
329
fi
330
od
331
332
for v in _narrows.keys do
333
if _is_heap_fact(v) /\ _created_by(_narrow_created_at, v, index) then
334
_append_crossing(_narrow_crossings, v, crossing)
335
fi
336
od
337
338
for p in _non_null_paths do
339
if _path_created_by(_path_presence_created_at, p, index) then
340
_append_path_crossing(_path_presence_crossings, p, crossing)
341
fi
342
od
343
344
for p in _path_narrows.keys do
345
if _path_created_by(_path_narrow_created_at, p, index) then
346
_append_path_crossing(_path_narrow_crossings, p, crossing)
347
fi
348
od
349
si
350
351
_created_by(map: Collections.MAP[Symbol, int], v: Symbol, index: int) -> bool is
352
let created mut = 0
353
354
if map.try_get_value(v, created ref) then
355
return created <= index
356
fi
357
358
return true
359
si
360
361
_path_created_by(map: Collections.MAP[ACCESS_PATH, int], p: ACCESS_PATH, index: int) -> bool is
362
let created mut = 0
363
364
if map.try_get_value(p, created ref) then
365
return created <= index
366
fi
367
368
return true
369
si
370
371
_append_crossing(
372
map: Collections.MAP[Symbol, Collections.LIST[CROSSING]],
373
v: Symbol,
374
crossing: CROSSING
375
) is
376
let existing: Collections.LIST[CROSSING] mut
377
378
if map.try_get_value(v, existing ref) then
379
existing.add(crossing)
380
else
381
let fresh = Collections.LIST[CROSSING]()
382
fresh.add(crossing)
383
map[v] = fresh
384
fi
385
si
386
387
_append_path_crossing(
388
map: Collections.MAP[ACCESS_PATH, Collections.LIST[CROSSING]],
389
p: ACCESS_PATH,
390
crossing: CROSSING
391
) is
392
let existing: Collections.LIST[CROSSING] mut
393
394
if map.try_get_value(p, existing ref) then
395
existing.add(crossing)
396
else
397
let fresh = Collections.LIST[CROSSING]()
398
fresh.add(crossing)
399
map[p] = fresh
400
fi
401
si
402
403
non_null_crossings_of(v: Symbol) -> Collections.LIST[CROSSING]? is
404
let result: Collections.LIST[CROSSING] mut
405
406
if _non_null_crossings.try_get_value(v, result ref) then
407
return result
408
fi
409
410
return null
411
si
412
413
narrow_crossings_of(v: Symbol) -> Collections.LIST[CROSSING]? is
414
let result: Collections.LIST[CROSSING] mut
415
416
if _narrow_crossings.try_get_value(v, result ref) then
417
return result
418
fi
419
420
return null
421
si
422
423
path_presence_crossings_of(p: ACCESS_PATH) -> Collections.LIST[CROSSING]? is
424
let result: Collections.LIST[CROSSING] mut
425
426
if _path_presence_crossings.try_get_value(p, result ref) then
427
return result
428
fi
429
430
return null
431
si
432
433
path_narrow_crossings_of(p: ACCESS_PATH) -> Collections.LIST[CROSSING]? is
434
let result: Collections.LIST[CROSSING] mut
435
436
if _path_narrow_crossings.try_get_value(p, result ref) then
437
return result
438
fi
439
440
return null
441
si
442
443
_clear_crossings(map: Collections.MAP[Symbol, Collections.LIST[CROSSING]], v: Symbol) is
444
if map.contains_key(v) then
445
map.remove(v)
446
fi
447
si
448
449
_clear_path_crossings(map: Collections.MAP[ACCESS_PATH, Collections.LIST[CROSSING]], p: ACCESS_PATH) is
450
if map.contains_key(p) then
451
map.remove(p)
452
fi
453
si
454
455
// The unreachable environment.
456
bottom() -> NARROW_ENV static is
457
let e = NARROW_ENV()
458
e.is_bottom = true
459
return e
460
si
461
462
is_empty: bool => !is_bottom /\ _narrows.count == 0
463
count: int => _narrows.count
464
465
variables: Collections.Iterable[Symbol] => _narrows.keys
466
467
contains(v: Symbol) -> bool => _narrows.contains_key(v)
468
469
// The narrowed type recorded for `v`, or null when `v` is
470
// not narrowed in this environment.
471
narrowed_type_of(v: Symbol) -> Type? is
472
let result: Type mut
473
474
if _narrows.try_get_value(v, result ref) then
475
return result
476
fi
477
478
return null
479
si
480
481
// Record `v` as narrowed to `t` (mutating). A bottom env
482
// ignores narrows — it stays unreachable.
483
//
484
// If `v` already carries a narrow `existing`, compose:
485
// `INTERSECTION.try_create(existing, t)`. The factory:
486
// - returns `t` when t strict-subtypes existing (refining),
487
// - returns `existing` when existing strict-subtypes t (the
488
// new narrow is weaker, no-op),
489
// - returns a multi-member intersection when both are
490
// real new facts (e.g., a class plus a sibling trait),
491
// - returns null when the two carry unrelated concrete
492
// identities — no runtime value can be both, so the edge
493
// recording the narrow is statically impossible. The code
494
// on that edge is written against the newly tested type
495
// (`elif isa TUPLE(x) then x.elements`), so `t` replaces
496
// the stale narrow: on an unreachable edge any view is
497
// sound, and the tested type is the one that typechecks.
498
// So stacked narrows like `if isa A(x) then if isa B(x)`
499
// produce `Declared & A & B` rather than dropping A.
500
//
501
// Skipped when composing with an existing narrow doesn't
502
// change the recorded type — every extra fact costs
503
// downstream copies, joins and kill iterations for no
504
// observable effect. A fresh record (no existing narrow) is
505
// always kept; if it turns out to no-op against v's declared
506
// type, the branch-entry `_apply_one` catches it.
507
set_narrow(v: Symbol, t: Type) is
508
if isa Semantic.Symbols.Property(v) /\ _declines_getter_fact(v, null, false) then
509
return
510
fi
511
512
if is_bottom then
513
return
514
fi
515
516
let existing: Type mut
517
518
if _narrows.try_get_value(v, existing ref) then
519
let composed = INTERSECTION.try_create(existing, t)
520
521
if !composed? then
522
// a fresh test superseding a statically-impossible
523
// prior narrow: nothing of the old fact survives
524
_narrows[v] = t
525
_clear_crossings(_narrow_crossings, v)
526
_narrow_created_at[v] = creation_mark
527
return
528
fi
529
530
if composed.matches(existing) then
531
// a re-test that alone implies the whole recorded
532
// fact re-proves it, crossings and all
533
if composed.matches(t) then
534
_clear_crossings(_narrow_crossings, v)
535
_narrow_created_at[v] = creation_mark
536
fi
537
538
return
539
fi
540
541
// composing keeps the prior component's crossings — and
542
// its creation index: the new test observed the value,
543
// the old fact did not — unless the new observation
544
// alone implies the whole composition
545
_narrows[v] = composed
546
547
if composed.matches(t) then
548
_clear_crossings(_narrow_crossings, v)
549
_narrow_created_at[v] = creation_mark
550
fi
551
else
552
_narrows[v] = t
553
_clear_crossings(_narrow_crossings, v)
554
_narrow_created_at[v] = creation_mark
555
fi
556
si
557
558
// Record `t` as the narrow for `v`, discarding any existing
559
// narrow instead of composing with it. For callers that have
560
// already computed the absolute narrowed type — the else-edge
561
// complement is derived from the current narrow, so it fully
562
// supersedes it. Composition would be wrong when the complement
563
// collapses to a supertype of the prior narrow (a concrete root
564
// that outlives every eliminated subclass): INTERSECTION would
565
// reinstate the wider prior narrow and the eliminated subclass
566
// would survive.
567
replace_narrow(v: Symbol, t: Type) is
568
if is_bottom then
569
return
570
fi
571
572
_narrows[v] = t
573
// The replacement derives from the test establishing it:
574
// crossings recorded before that test cannot invalidate it,
575
// however much of the prior fact's record is inherited.
576
_narrow_created_at[v] = creation_mark
577
si
578
579
// Path-keyed mirror of `replace_narrow`.
580
replace_path_narrow(path: ACCESS_PATH, t: Type) is
581
if is_bottom then
582
return
583
fi
584
585
_path_narrows[path] = t
586
_path_narrow_created_at[path] = creation_mark
587
si
588
589
// Forget any narrow for `v` (mutating) — used by the
590
// assignment transfer function.
591
drop_narrow(v: Symbol) is
592
if _narrows.contains_key(v) then
593
_narrows.remove(v)
594
fi
595
si
596
597
// The locals definitely assigned at this point.
598
assigned_variables: Collections.Iterable[Symbol] => _assigned
599
600
// True iff `v` is definitely assigned at this point.
601
is_assigned(v: Symbol) -> bool => _assigned.contains(v)
602
603
// The methods definitely called at this point.
604
called_methods: Collections.Iterable[Symbol] => _called
605
606
is_called(f: Symbol) -> bool => _called.contains(f)
607
608
set_called(f: Symbol) is
609
_called.add(f)
610
si
611
612
// Record `v` as definitely assigned (mutating). A bottom env
613
// ignores it — it stays unreachable.
614
set_assigned(v: Symbol) is
615
if is_bottom then
616
return
617
fi
618
619
_assigned.add(v)
620
si
621
622
// The locals known to hold a value at this point.
623
non_null_variables: Collections.Iterable[Symbol] => _non_null
624
625
// True iff `v` is known to hold a value at this point.
626
is_non_null(v: Symbol) -> bool => _non_null.contains(v)
627
628
// Record `v` as known to hold a value (mutating). A bottom
629
// env ignores it — it stays unreachable.
630
//
631
// Callers should filter out presence facts on non-optional
632
// targets themselves: NARROW_ENV can't safely check `v.type`
633
// here because the symbol's live type reflects whatever the
634
// outer walk state applied, not the fresh env being built —
635
// a redundant-looking fact recorded on a nested if body may
636
// still be observable at the branch entry when the outer
637
// narrow is restored.
638
set_non_null(v: Symbol) is
639
if isa Semantic.Symbols.Property(v) /\ _declines_getter_fact(v, null, true) then
640
return
641
fi
642
643
if is_bottom then
644
return
645
fi
646
647
_non_null.add(v)
648
_clear_crossings(_non_null_crossings, v)
649
_non_null_created_at[v] = creation_mark
650
si
651
652
// Forget that `v` is known to hold a value (mutating) — used
653
// by the assignment transfer function.
654
drop_non_null(v: Symbol) is
655
if _non_null.contains(v) then
656
_non_null.remove(v)
657
fi
658
si
659
660
// The member-access paths known to hold a value at this point.
661
non_null_paths: Collections.Iterable[ACCESS_PATH] => _non_null_paths
662
663
// True iff `path` is known to hold a value at this point.
664
is_non_null_path(path: ACCESS_PATH) -> bool => _non_null_paths.contains(path)
665
666
// Record `path` as known to hold a value (mutating). A bottom
667
// env ignores it — it stays unreachable.
668
set_non_null_path(path: ACCESS_PATH) is
669
if path.has_getter_hop /\ _declines_getter_fact(null, path, true) then
670
return
671
fi
672
673
if is_bottom then
674
return
675
fi
676
677
_non_null_paths.add(path)
678
_clear_path_crossings(_path_presence_crossings, path)
679
_path_presence_created_at[path] = creation_mark
680
si
681
682
// The member-access paths carrying a type narrow at this point.
683
narrowed_paths: Collections.Iterable[ACCESS_PATH] => _path_narrows.keys
684
685
// The narrowed type recorded for `path`, or null when it is
686
// not narrowed in this environment.
687
narrowed_type_of_path(path: ACCESS_PATH) -> Type? is
688
let result: Type mut
689
690
if _path_narrows.try_get_value(path, result ref) then
691
return result
692
fi
693
694
return null
695
si
696
697
// Record `path` as narrowed to `t` (mutating). Bottom env
698
// ignores it. Stacks like `set_narrow` — a prior narrow on
699
// the same path composes with `t` via INTERSECTION, and an
700
// unrelated-concretes pair (statically-impossible test edge)
701
// replaces the stale narrow with `t` for the same reason.
702
set_path_narrow(path: ACCESS_PATH, t: Type) is
703
if path.has_getter_hop /\ _declines_getter_fact(null, path, false) then
704
return
705
fi
706
707
if is_bottom then
708
return
709
fi
710
711
let existing: Type mut
712
713
if _path_narrows.try_get_value(path, existing ref) then
714
let composed = INTERSECTION.try_create(existing, t)
715
716
_path_narrows[path] = if composed? then composed else t fi
717
718
if !composed? \/ composed.matches(t) then
719
_clear_path_crossings(_path_narrow_crossings, path)
720
_path_narrow_created_at[path] = creation_mark
721
fi
722
else
723
_path_narrows[path] = t
724
_clear_path_crossings(_path_narrow_crossings, path)
725
_path_narrow_created_at[path] = creation_mark
726
fi
727
si
728
729
// Forget every tracked path — the call transfer: a
730
// possibly-storing callee may have changed anything a path
731
// reads. Same reasoning for path type narrows.
732
drop_all_paths() is
733
_non_null_paths.clear()
734
_path_narrows.clear()
735
si
736
737
// Forget every path that reads through a property getter —
738
// the heap-store transfer: a store anywhere can change what a
739
// getter returns. Fields-only paths survive; a store to a
740
// field they read through is handled by drop_paths_through.
741
// Applies to both presence and type-narrow slots.
742
drop_getter_paths() is
743
let doomed = Collections.LIST[ACCESS_PATH]()
744
745
for p in _non_null_paths do
746
if p.has_getter_hop then
747
doomed.add(p)
748
fi
749
od
750
751
for p in doomed do
752
_non_null_paths.remove(p)
753
od
754
755
let doomed_narrows = Collections.LIST[ACCESS_PATH]()
756
757
for p in _path_narrows.keys do
758
if p.has_getter_hop then
759
doomed_narrows.add(p)
760
fi
761
od
762
763
for p in doomed_narrows do
764
_path_narrows.remove(p)
765
od
766
si
767
768
// Remove every fact the solved relations cannot back across
769
// this crossing, recording each kill for the ledger.
770
_apply_relations_to_self(crossing: CROSSING, index: int) is
771
let doomed_narrows = Collections.LIST[Symbol]()
772
773
for v in _narrows.keys do
774
if
775
_is_heap_fact(v) /\
776
_created_by(_narrow_created_at, v, index) /\
777
!RELIANCES.crossing_discharged(crossing, v, null, false)
778
then
779
let kill = KILL(crossing.location.file_name, crossing, v, null, false, true)
780
781
kill.narrowed_type = narrowed_type_of(v)
782
783
KILL_LEDGER.record(kill)
784
doomed_narrows.add(v)
785
fi
786
od
787
788
for v in doomed_narrows do
789
drop_narrow(v)
790
od
791
792
let doomed_presence = Collections.LIST[Symbol]()
793
794
for v in _non_null do
795
if
796
_is_heap_fact(v) /\
797
_created_by(_non_null_created_at, v, index) /\
798
!RELIANCES.crossing_discharged(crossing, v, null, true)
799
then
800
KILL_LEDGER.record(KILL(crossing.location.file_name, crossing, v, null, true, true))
801
doomed_presence.add(v)
802
fi
803
od
804
805
for v in doomed_presence do
806
drop_non_null(v)
807
od
808
809
let doomed_paths = Collections.LIST[ACCESS_PATH]()
810
811
for p in _non_null_paths do
812
if
813
_path_created_by(_path_presence_created_at, p, index) /\
814
!RELIANCES.crossing_discharged(crossing, null, p, true)
815
then
816
KILL_LEDGER.record(KILL(crossing.location.file_name, crossing, null, p, true, true))
817
doomed_paths.add(p)
818
fi
819
od
820
821
for p in _path_narrows.keys do
822
if
823
_path_created_by(_path_narrow_created_at, p, index) /\
824
!RELIANCES.crossing_discharged(crossing, null, p, false)
825
then
826
let kill = KILL(crossing.location.file_name, crossing, null, p, false, true)
827
828
kill.narrowed_type = narrowed_type_of_path(p)
829
830
KILL_LEDGER.record(kill)
831
doomed_paths.add(p)
832
fi
833
od
834
835
for p in doomed_paths do
836
drop_path(p)
837
od
838
si
839
840
// Forget one exact path, both slots — the in-walk kill
841
// transfer drops a single fact rather than a class of them.
842
drop_path(path: ACCESS_PATH) is
843
_non_null_paths.remove(path)
844
_path_narrows.remove(path)
845
si
846
847
// Forget every path that reads through `member` at any hop —
848
// the member-store transfer. Keyed on the member symbol, not
849
// the written receiver, so a store through an aliased
850
// receiver still invalidates. Applies to both presence and
851
// type-narrow slots.
852
drop_paths_through(member: Symbol) is
853
let doomed = Collections.LIST[ACCESS_PATH]()
854
855
for p in _non_null_paths do
856
if p.contains_member(member) then
857
doomed.add(p)
858
fi
859
od
860
861
for p in doomed do
862
_non_null_paths.remove(p)
863
od
864
865
let doomed_narrows = Collections.LIST[ACCESS_PATH]()
866
867
for p in _path_narrows.keys do
868
if p.contains_member(member) then
869
doomed_narrows.add(p)
870
fi
871
od
872
873
for p in doomed_narrows do
874
_path_narrows.remove(p)
875
od
876
si
877
878
// Forget every tracked path rooted at `root` — used when the
879
// root is reassigned, which redirects every `root.…` path.
880
// Applies to both presence and type-narrow slots.
881
drop_paths_rooted_at(root: Symbol) is
882
let doomed = Collections.LIST[ACCESS_PATH]()
883
884
for p in _non_null_paths do
885
if p.root == root then
886
doomed.add(p)
887
fi
888
od
889
890
for p in doomed do
891
_non_null_paths.remove(p)
892
od
893
894
let doomed_narrows = Collections.LIST[ACCESS_PATH]()
895
896
for p in _path_narrows.keys do
897
if p.root == root then
898
doomed_narrows.add(p)
899
fi
900
od
901
902
for p in doomed_narrows do
903
_path_narrows.remove(p)
904
od
905
si
906
907
// Forget every fact a heap mutation can invalidate: narrows
908
// and presence facts keyed on fields or properties, and every
909
// member-access path. Local-variable facts survive — a callee
910
// cannot reach a local. Used by controlled walks whose branch
911
// environments derive from a snapshot taken before the walk:
912
// when the walk killed heap facts, the snapshot's heap facts
913
// cannot be trusted on the edges built from it.
914
drop_heap_facts() is
915
let stale = Collections.LIST[Symbol]()
916
917
for v in _narrows.keys do
918
if isa Semantic.Symbols.Field(v) \/ isa Semantic.Symbols.Property(v) then
919
stale.add(v)
920
fi
921
od
922
923
for v in stale do
924
_narrows.remove(v)
925
od
926
927
let stale_presence = Collections.LIST[Symbol]()
928
929
for v in _non_null do
930
if isa Semantic.Symbols.Field(v) \/ isa Semantic.Symbols.Property(v) then
931
stale_presence.add(v)
932
fi
933
od
934
935
for v in stale_presence do
936
_non_null.remove(v)
937
od
938
939
_non_null_paths.clear()
940
_path_narrows.clear()
941
_non_null_crossings.clear()
942
_narrow_crossings.clear()
943
_path_presence_crossings.clear()
944
_path_narrow_crossings.clear()
945
_non_null_created_at.clear()
946
_narrow_created_at.clear()
947
_path_presence_created_at.clear()
948
_path_narrow_created_at.clear()
949
si
950
951
copy() -> NARROW_ENV is
952
let e = NARROW_ENV()
953
e.is_bottom = is_bottom
954
955
for v in _narrows.keys do
956
e._narrows[v] = _narrows[v]
957
od
958
959
for v in _assigned do
960
e._assigned.add(v)
961
od
962
963
for f in _called do
964
e._called.add(f)
965
od
966
967
for v in _non_null do
968
e._non_null.add(v)
969
od
970
971
for p in _non_null_paths do
972
e._non_null_paths.add(p)
973
od
974
975
for p in _path_narrows.keys do
976
e._path_narrows[p] = _path_narrows[p]
977
od
978
979
for v in _non_null_crossings.keys do
980
e._non_null_crossings[v] = _copied_crossings(_non_null_crossings[v])
981
od
982
983
for v in _narrow_crossings.keys do
984
e._narrow_crossings[v] = _copied_crossings(_narrow_crossings[v])
985
od
986
987
for p in _path_presence_crossings.keys do
988
e._path_presence_crossings[p] = _copied_crossings(_path_presence_crossings[p])
989
od
990
991
for p in _path_narrow_crossings.keys do
992
e._path_narrow_crossings[p] = _copied_crossings(_path_narrow_crossings[p])
993
od
994
995
for v in _non_null_created_at.keys do
996
e._non_null_created_at[v] = _non_null_created_at[v]
997
od
998
999
for v in _narrow_created_at.keys do
1000
e._narrow_created_at[v] = _narrow_created_at[v]
1001
od
1002
1003
for p in _path_presence_created_at.keys do
1004
e._path_presence_created_at[p] = _path_presence_created_at[p]
1005
od
1006
1007
for p in _path_narrow_created_at.keys do
1008
e._path_narrow_created_at[p] = _path_narrow_created_at[p]
1009
od
1010
1011
return e
1012
si
1013
1014
// Carry another env's crossing records into this one — used
1015
// by the flow's set_env, which rebuilds the applied env
1016
// through the set_* methods, whose establishment semantics
1017
// would otherwise clear the records at every branch entry.
1018
adopt_crossings_from(source: NARROW_ENV) is
1019
for v in source._non_null_crossings.keys do
1020
_non_null_crossings[v] = _copied_crossings(source._non_null_crossings[v])
1021
od
1022
1023
for v in source._narrow_crossings.keys do
1024
_narrow_crossings[v] = _copied_crossings(source._narrow_crossings[v])
1025
od
1026
1027
for p in source._path_presence_crossings.keys do
1028
_path_presence_crossings[p] = _copied_crossings(source._path_presence_crossings[p])
1029
od
1030
1031
for p in source._path_narrow_crossings.keys do
1032
_path_narrow_crossings[p] = _copied_crossings(source._path_narrow_crossings[p])
1033
od
1034
si
1035
1036
// Carry another env's fact creation indexes into this one —
1037
// paired with `adopt_crossings_from` in the flow's set_env, so
1038
// a rebuilt environment keeps the indexes its facts were
1039
// established under.
1040
adopt_creation_marks_from(source: NARROW_ENV) is
1041
for v in source._non_null_created_at.keys do
1042
_non_null_created_at[v] = source._non_null_created_at[v]
1043
od
1044
1045
for v in source._narrow_created_at.keys do
1046
_narrow_created_at[v] = source._narrow_created_at[v]
1047
od
1048
1049
for p in source._path_presence_created_at.keys do
1050
_path_presence_created_at[p] = source._path_presence_created_at[p]
1051
od
1052
1053
for p in source._path_narrow_created_at.keys do
1054
_path_narrow_created_at[p] = source._path_narrow_created_at[p]
1055
od
1056
si
1057
1058
_copied_crossings(crossings: Collections.LIST[CROSSING]) -> Collections.LIST[CROSSING] static is
1059
let result = Collections.LIST[CROSSING]()
1060
1061
for c in crossings do
1062
result.add(c)
1063
od
1064
1065
return result
1066
si
1067
1068
// A copy carrying only the definite-assignment facts — no
1069
// narrows, reachable. Used where the narrowing facts must be
1070
// discarded conservatively (after a try statement) but
1071
// assignments made before it still hold.
1072
with_only_assigned() -> NARROW_ENV is
1073
let e = NARROW_ENV()
1074
1075
for v in _assigned do
1076
e._assigned.add(v)
1077
od
1078
1079
for f in _called do
1080
e._called.add(f)
1081
od
1082
1083
return e
1084
si
1085
1086
// A copy carrying the definite-assignment facts and, of the
1087
// narrowing facts, those on stack locals that `keep` accepts.
1088
// Used for the entry to a finally body, which runs on every
1089
// path out of its try: a fact proven before the try still
1090
// holds there unless the try writes what it was proven on.
1091
// Heap facts are left out - a call anywhere in the try can
1092
// invalidate one, and the crossings that would judge that
1093
// were recorded in the body's environment rather than this
1094
// one.
1095
with_only_assigned_and_kept_locals(keep: (Symbol) -> bool) -> NARROW_ENV is
1096
let e = with_only_assigned()
1097
1098
for v in _non_null do
1099
if !_is_heap_fact(v) /\ keep(v) then
1100
e.set_non_null(v)
1101
fi
1102
od
1103
1104
for v in _narrows.keys do
1105
if !_is_heap_fact(v) /\ keep(v) then
1106
if let t = narrowed_type_of(v) then
1107
e.set_narrow(v, t)
1108
fi
1109
fi
1110
od
1111
1112
return e
1113
si
1114
1115
// Merge two environments at a control-flow merge point.
1116
// `bottom` is the identity (a diverging branch contributes
1117
// nothing). Every domain merges by intersection: a variable
1118
// survives the narrowing merge only when narrowed in BOTH
1119
// inputs (merged type = least upper bound); a variable is
1120
// definitely assigned after the merge only when assigned in
1121
// BOTH inputs; likewise known-non-null only when in both.
1122
join(a: NARROW_ENV, b: NARROW_ENV) -> NARROW_ENV static is
1123
if a.is_bottom then
1124
return b.copy()
1125
fi
1126
1127
if b.is_bottom then
1128
return a.copy()
1129
fi
1130
1131
let result = NARROW_ENV()
1132
1133
for v in a.variables do
1134
if let ta = a.narrowed_type_of(v), tb = b.narrowed_type_of(v) then
1135
// Two narrows over the same closed root join by
1136
// subtype-set union — exact, and it keeps the
1137
// optional flag when either edge can be null.
1138
// The general LUB is the fallback for every
1139
// other shape.
1140
let merged mut = Semantic.Types.ONE_OF.try_join(ta, tb)
1141
1142
if !merged? then
1143
let lub = Semantic.LEAST_UPPER_BOUND_MAP()
1144
lub.add(ta)
1145
lub.add(tb)
1146
1147
merged = lub.get_result()
1148
fi
1149
1150
if merged? then
1151
result.set_narrow(v, merged)
1152
fi
1153
fi
1154
od
1155
1156
for v in a.assigned_variables do
1157
if b.is_assigned(v) then
1158
result.set_assigned(v)
1159
fi
1160
od
1161
1162
for f in a.called_methods do
1163
if b.is_called(f) then
1164
result.set_called(f)
1165
fi
1166
od
1167
1168
for v in a.non_null_variables do
1169
if b.is_non_null(v) then
1170
result.set_non_null(v)
1171
fi
1172
od
1173
1174
for p in a.non_null_paths do
1175
if b.is_non_null_path(p) then
1176
result.set_non_null_path(p)
1177
fi
1178
od
1179
1180
for p in a.narrowed_paths do
1181
if let ta = a.narrowed_type_of_path(p), tb = b.narrowed_type_of_path(p) then
1182
// Two narrows over the same closed root join by
1183
// subtype-set union — mirrors the symbol case.
1184
let merged mut = Semantic.Types.ONE_OF.try_join(ta, tb)
1185
1186
if !merged? then
1187
let lub = Semantic.LEAST_UPPER_BOUND_MAP()
1188
lub.add(ta)
1189
lub.add(tb)
1190
1191
merged = lub.get_result()
1192
fi
1193
1194
if merged? then
1195
result.set_path_narrow(p, merged)
1196
fi
1197
fi
1198
od
1199
1200
// A fact that survives the merge survived every crossing
1201
// on either edge, so its record is the union of both.
1202
// The set_* calls above cleared the result's entries, so
1203
// the union is written afterwards.
1204
for v in result._non_null do
1205
_merge_crossings(result._non_null_crossings, v, a.non_null_crossings_of(v), b.non_null_crossings_of(v))
1206
result._non_null_created_at[v] = _merged_created(a._non_null_created_at, b._non_null_created_at, v)
1207
od
1208
1209
for v in result._narrows.keys do
1210
_merge_crossings(result._narrow_crossings, v, a.narrow_crossings_of(v), b.narrow_crossings_of(v))
1211
result._narrow_created_at[v] = _merged_created(a._narrow_created_at, b._narrow_created_at, v)
1212
od
1213
1214
for p in result._non_null_paths do
1215
_merge_path_crossings(result._path_presence_crossings, p, a.path_presence_crossings_of(p), b.path_presence_crossings_of(p))
1216
result._path_presence_created_at[p] = _merged_path_created(a._path_presence_created_at, b._path_presence_created_at, p)
1217
od
1218
1219
for p in result._path_narrows.keys do
1220
_merge_path_crossings(result._path_narrow_crossings, p, a.path_narrow_crossings_of(p), b.path_narrow_crossings_of(p))
1221
result._path_narrow_created_at[p] = _merged_path_created(a._path_narrow_created_at, b._path_narrow_created_at, p)
1222
od
1223
1224
return result
1225
si
1226
1227
// The creation index of a fact surviving a join: the earlier
1228
// edge's index when both edges record one, zero otherwise — a
1229
// side with no recorded index may have carried the fact since
1230
// the start of the log, and the merged record inherits its
1231
// crossings, so nothing may be filtered.
1232
_merged_created(
1233
from_a: Collections.MAP[Symbol, int],
1234
from_b: Collections.MAP[Symbol, int],
1235
v: Symbol
1236
) -> int static is
1237
let mark_a mut = 0
1238
let mark_b mut = 0
1239
1240
if from_a.try_get_value(v, mark_a ref) /\ from_b.try_get_value(v, mark_b ref) then
1241
return if mark_a < mark_b then mark_a else mark_b fi
1242
fi
1243
1244
return 0
1245
si
1246
1247
_merged_path_created(
1248
from_a: Collections.MAP[ACCESS_PATH, int],
1249
from_b: Collections.MAP[ACCESS_PATH, int],
1250
p: ACCESS_PATH
1251
) -> int static is
1252
let mark_a mut = 0
1253
let mark_b mut = 0
1254
1255
if from_a.try_get_value(p, mark_a ref) /\ from_b.try_get_value(p, mark_b ref) then
1256
return if mark_a < mark_b then mark_a else mark_b fi
1257
fi
1258
1259
return 0
1260
si
1261
1262
_merge_crossings(
1263
map: Collections.MAP[Symbol, Collections.LIST[CROSSING]],
1264
v: Symbol,
1265
from_a: Collections.LIST[CROSSING]?,
1266
from_b: Collections.LIST[CROSSING]?
1267
) static is
1268
let merged = _merged_crossing_lists(from_a, from_b)
1269
1270
if merged? then
1271
map[v] = merged
1272
fi
1273
si
1274
1275
_merge_path_crossings(
1276
map: Collections.MAP[ACCESS_PATH, Collections.LIST[CROSSING]],
1277
p: ACCESS_PATH,
1278
from_a: Collections.LIST[CROSSING]?,
1279
from_b: Collections.LIST[CROSSING]?
1280
) static is
1281
let merged = _merged_crossing_lists(from_a, from_b)
1282
1283
if merged? then
1284
map[p] = merged
1285
fi
1286
si
1287
1288
_merged_crossing_lists(
1289
from_a: Collections.LIST[CROSSING]?,
1290
from_b: Collections.LIST[CROSSING]?
1291
) -> Collections.LIST[CROSSING]? static is
1292
if !from_a? /\ !from_b? then
1293
return null
1294
fi
1295
1296
let merged = Collections.LIST[CROSSING]()
1297
1298
if from_a? then
1299
for c in from_a do
1300
merged.add(c)
1301
od
1302
fi
1303
1304
if from_b? then
1305
for c in from_b do
1306
// both edges usually share the pre-branch prefix;
1307
// skip exact duplicates so the record stays small
1308
if !merged.contains(c) then
1309
merged.add(c)
1310
fi
1311
od
1312
fi
1313
1314
return merged
1315
si
1316
si
1317
si