Skip to content
← Back

src/syntax/process/narrowing_flow.ghul

1
namespace Syntax.Process is
2
use Logging.Logger;
3
use Source.LOCATION;
4
use Semantic.Types.Type;
5
use Semantic.Types.INTERSECTION;
6
use Symbol = Semantic.Symbols.Symbol;
7
8
// Per-IF bookkeeping for the flow pass. pre(IF) pushes a frame;
9
// each branch's controlled walk records its body-exit
10
// environment and threads the running else-environment to the
11
// next branch; visit(IF) joins the branch exits.
12
class IF_FLOW_FRAME is
13
// Environment for the next not-yet-walked branch — refined
14
// by each cond branch's false edge as the chain proceeds.
15
running_else: NARROW_ENV public;
16
// Exit environment of each branch already walked (bottom for
17
// a branch whose body diverges).
18
branch_exits: Collections.LIST[NARROW_ENV] public;
19
// True once an unconditional (else) branch has been seen.
20
has_else: bool public;
21
22
init(in_env: NARROW_ENV) is
23
running_else = in_env;
24
branch_exits = Collections.LIST[NARROW_ENV]();
25
si
26
si
27
28
// Per-`try` bookkeeping for the flow pass. pre(TRY) pushes a
29
// frame; pre(CATCH) observes the try body's exit, visit(CATCH)
30
// each handler's exit, and visit(TRY) the no-catch body exit, so
31
// visit(TRY) can tell whether control can fall through the try
32
// statement at all (it cannot when the body and every handler
33
// diverge).
34
class TRY_FLOW_FRAME is
35
// The environment in force before the try — its
36
// definite-assignment facts survive the statement.
37
entry: NARROW_ENV public;
38
// True once the try body's exit environment has been
39
// observed (at the first catch, or — with no catches — at
40
// visit(TRY)).
41
body_seen: bool public;
42
// True if the body, or some catch handler, can complete
43
// normally and so reach the end of the try statement.
44
can_complete: bool public;
45
46
init(entry: NARROW_ENV) is
47
self.entry = entry;
48
si
49
si
50
51
// The flow-sensitive narrowing orchestrator. Holds the narrowing
52
// environment in force at the current walk point and keeps each
53
// narrowed variable's `Symbol.type` reconciled to it, so the
54
// rest of COMPILE_EXPRESSIONS — and the per-use-site IR value
55
// snapshots the IDE reads — observe the narrowed type.
56
//
57
// Replaces the scope-stack NARROWING. See
58
// `docs/claude/flow-sensitive-narrowing.md`.
59
class NARROWING_FLOW is
60
// Sink for the analysis-mode narrowing-kill hints. Its
61
// `want_hint_for` gate and `hint` are the only members consulted
62
// here.
63
_logger: Logger;
64
65
_current_env: NARROW_ENV;
66
// Declared (pre-narrowing) type of every variable that has
67
// been narrowed — the restore target. Captured lazily the
68
// first time a variable is narrowed.
69
_declared: Collections.MAP[Symbol, Type];
70
71
// Deferred-initialization locals (`let x: T;` with no
72
// initializer) subject to the definite-assignment read
73
// check. Method-global within one body walk — a variable,
74
// once declared, stays tracked until the next reset.
75
_tracked: Collections.SET[Symbol];
76
77
// Assignment-target receivers held exempt from the call transfer
78
// while their assignment is in flight. See RECEIVER_SHIELD.
79
_shield: RECEIVER_SHIELD;
80
81
// Bumped by every heap-fact kill (call, heap store, member
82
// store). Controlled walks capture it before walking and
83
// compare after: branch environments derived from a snapshot
84
// taken before a walk that killed cannot keep the snapshot's
85
// heap facts. `_current_env` itself always reflects kills
86
// directly, so straight-line flow needs no epoch check.
87
_heap_epoch: int;
88
89
// Speculation baselines, mirroring the logger's diagnostics
90
// stack. A speculative expression re-walk pushes the pre-walk
91
// env, resets to it between retry attempts, and on completion
92
// either commits (keeps the final walk's facts) or rolls back
93
// (discards them). This keeps the flow env and the coupled
94
// in-place `Symbol.type` narrowing from desyncing when a
95
// re-walk's diagnostics are scrubbed.
96
_speculation: Collections.STACK[NARROW_ENV];
97
98
init(logger: Logger) is
99
_logger = logger;
100
_current_env = NARROW_ENV();
101
_declared = Collections.MAP[Symbol, Type]();
102
_tracked = Collections.SET[Symbol]();
103
_shield = RECEIVER_SHIELD();
104
_speculation = Collections.STACK[NARROW_ENV]();
105
si
106
107
// Surface a hint at the point a narrowing is discarded, so the
108
// editor can show where and why a narrowed type stops
109
// applying. Emitted only in analysis mode, and only for a
110
// variable that actually carries a narrow at this point —
111
// `reason` names what invalidated it. Must be called before
112
// the narrow is dropped, while `_current_env` still holds it.
113
_report_kill(v: Symbol, location: LOCATION, reason: string) is
114
// Editor-only: never generate a kill hint for a file the client
115
// is not viewing — it would be invisible there and only add
116
// formatting and transmission cost.
117
if !_logger.want_hint_for(location) then
118
return;
119
fi
120
121
let narrowed = _current_env.narrowed_type_of(v);
122
let declared = declared_type_of(v);
123
124
if narrowed? then
125
_logger.inlay(location, "narrowing-killed", "◄", "{v.name}\n{declared}\n\n{reason}");
126
elif _current_env.is_non_null(v) then
127
// The presence bit is redundant with a non-optional
128
// declared type — a non-optional variable is always
129
// known to hold a value, so the bit adds nothing over
130
// the declaration and the kill hint would confuse the
131
// reader by pointing at a narrowing that never carried
132
// information.
133
if declared? /\ declared.is_optional then
134
_logger.inlay(location, "narrowing-killed", "◄", "{v.name}\n{declared}\n\n{reason}");
135
fi
136
fi
137
si
138
139
// Path analogue of `_report_kill`: surface a hint at the point
140
// a member-access path's narrow or presence fact is discarded.
141
// A path presence fact is only ever recorded for an optional
142
// access, so — unlike the symbol case — there is no redundant
143
// non-optional branch to guard. Same analysis-mode + open-file
144
// gating; must be called while `_current_env` still holds the
145
// fact.
146
_report_path_kill(path: ACCESS_PATH, location: LOCATION, reason: string) is
147
if !_logger.want_hint_for(location) then
148
return;
149
fi
150
151
let narrowed = _current_env.narrowed_type_of_path(path);
152
153
// A member-access path has no single declared type to name as
154
// the revert target, so the code body is just the path; the
155
// reason carries what changed.
156
if narrowed? then
157
_logger.inlay(location, "narrowing-killed", "◄", "{path}\n\n{reason}");
158
elif _current_env.is_non_null_path(path) then
159
_logger.inlay(location, "narrowing-killed", "◄", "{path}\n\n{reason}");
160
fi
161
si
162
163
current_env: NARROW_ENV => _current_env;
164
is_unreachable: bool => _current_env.is_bottom;
165
166
heap_epoch: int => _heap_epoch;
167
168
// True iff a heap-fact kill has fired since `epoch` was
169
// captured.
170
heap_killed_since(epoch: int) -> bool => _heap_epoch != epoch;
171
172
// While a function literal's body compiles, the transfers
173
// also record whether it performed any possibly heap-visible
174
// operation — the signal behind Function.literal_body_impure.
175
// A stack because literals nest; only the innermost literal
176
// is charged (an outer literal that never invokes the inner
177
// one is unaffected by it).
178
_literal_impure_stack: Collections.LIST[bool]?;
179
180
push_literal_frame() is
181
if !_literal_impure_stack? then
182
_literal_impure_stack = Collections.LIST[bool]();
183
fi
184
185
_literal_impure_stack.add(false);
186
si
187
188
pop_literal_frame() -> bool is
189
let stack = _literal_impure_stack;
190
191
assert stack?;
192
193
let result = stack[stack.count - 1];
194
stack.remove_at(stack.count - 1);
195
return result;
196
si
197
198
_mark_literal_impure() is
199
if _literal_impure_stack? /\ _literal_impure_stack.count > 0 then
200
_literal_impure_stack[_literal_impure_stack.count - 1] = true;
201
fi
202
si
203
204
// Narrowing subjects are local variables, fields and
205
// properties — all carry a settable type; anything else never
206
// enters the environment, but stay defensive.
207
_set_symbol_type(v: Symbol, t: Type) is
208
if isa Semantic.Types.SettableTyped(v) then
209
(cast Semantic.Types.SettableTyped(v)).set_type(t);
210
fi
211
si
212
213
// Restore every currently-narrowed variable to its declared
214
// type, empty the environment, and forget declared types.
215
// Called at each method-body boundary.
216
reset() is
217
restore_all();
218
_current_env = NARROW_ENV();
219
_declared.clear();
220
_tracked.clear();
221
222
// literal frames are pushed and popped around literal
223
// body walks; an aborted walk (analysis-mode exception)
224
// can leak one, so drop any leftovers at body boundaries
225
if _literal_impure_stack? then
226
_literal_impure_stack.clear();
227
fi
228
229
// Speculation baselines are balanced by the `speculate_then_*`
230
// disposables, but an aborted walk can leak one; drop any
231
// leftovers at the body boundary (inert env copies — nothing
232
// to restore, restore_all already reconciled `Symbol.type`).
233
_speculation.clear();
234
si
235
236
// Restore every currently-narrowed variable's `.type` to its
237
// declared type (without emptying the environment).
238
restore_all() is
239
for v in _current_env.variables do
240
let declared: Type mut;
241
242
if _declared.try_get_value(v, declared ref) then
243
_set_symbol_type(v, declared);
244
fi
245
od
246
si
247
248
// The declared (pre-narrowing) type of `v` — used for the
249
// assignment-LHS typecheck, which must see past any narrow.
250
declared_type_of(v: Symbol) -> Type? is
251
let declared: Type mut;
252
253
if _declared.try_get_value(v, declared ref) then
254
return declared;
255
fi
256
257
return v.type;
258
si
259
260
// Make `target` the environment in force: restore all
261
// current narrows, then apply `target`'s sound narrows,
262
// reconciling `Symbol.type` to match. `_current_env`
263
// becomes the actually-applied subset of `target`.
264
set_env(target: NARROW_ENV) is
265
restore_all();
266
267
let applied = NARROW_ENV();
268
269
if target.is_bottom then
270
applied.is_bottom = true;
271
else
272
for v in target.variables do
273
if let t = target.narrowed_type_of(v) then
274
if _apply_one(v, t) then
275
applied.set_narrow(v, t);
276
fi
277
fi
278
od
279
280
for v in target.assigned_variables do
281
applied.set_assigned(v);
282
od
283
284
for v in target.non_null_variables do
285
applied.set_non_null(v);
286
od
287
288
for p in target.non_null_paths do
289
applied.set_non_null_path(p);
290
od
291
292
for p in target.narrowed_paths do
293
if let t = target.narrowed_type_of_path(p) then
294
applied.set_path_narrow(p, t);
295
fi
296
od
297
fi
298
299
_current_env = applied;
300
si
301
302
// Snapshot the current env as a speculation baseline (see
303
// `_speculation`). Pairs with `commit` or `roll_back`; usually
304
// reached through the `speculate_then_*` disposables.
305
speculate() is
306
_speculation.push(_current_env.copy());
307
si
308
309
// Reset the current env to the active baseline without dropping
310
// it, so a retry re-walk starts from the facts the first walk
311
// saw rather than the ones it recorded. A no-op when no
312
// speculation is active, so callers that may or may not run
313
// inside a speculation can call it unconditionally.
314
restore() is
315
if _speculation.count > 0 then
316
set_env(_speculation.peek());
317
fi
318
si
319
320
// Drop the active baseline, keeping the current env — the
321
// walk's narrowing facts survive into the enclosing expression.
322
commit() is
323
assert _speculation.count >= 1 else "flow commit with no active speculation";
324
325
_speculation.pop();
326
si
327
328
// Drop the active baseline and restore the current env to it,
329
// discarding the speculative walk's narrowing facts.
330
roll_back() is
331
assert _speculation.count >= 1 else "flow roll_back with no active speculation";
332
333
set_env(_speculation.pop());
334
si
335
336
speculate_then_commit() -> FLOW_SPECULATE_THEN_COMMIT =>
337
FLOW_SPECULATE_THEN_COMMIT(self);
338
339
speculate_then_roll_back() -> FLOW_SPECULATE_THEN_ROLL_BACK =>
340
FLOW_SPECULATE_THEN_ROLL_BACK(self);
341
342
// Mark the current point unreachable (after return / throw /
343
// break / continue).
344
set_unreachable() is
345
set_env(NARROW_ENV.bottom());
346
si
347
348
// Assignment transfer plus re-narrow: a write to `v`
349
// invalidates any narrow on it and any presence fact, since
350
// the new value may not satisfy them; when the assigned
351
// value's static type is a strict refinement of the declared
352
// type, `v` then re-narrows to it. Returns the narrowed-to
353
// view when one was applied, else null.
354
//
355
// The editor hint reflects which of the two halves fired: a
356
// re-narrow to a different view renders both edges in one
357
// hint (the killed view and the new one) with the usual
358
// reassignment note; a re-narrow to the same view is a plain
359
// introduction; a kill with no new narrow keeps the plain
360
// kill hint.
361
on_assignment(v: Symbol, location: LOCATION, value_type: Type?) -> Type? is
362
// The in-force facts, captured before the transfer drops
363
// them, so the hint can name the killed view.
364
let killed_narrow = _current_env.narrowed_type_of(v);
365
let killed_presence = _current_env.is_non_null(v);
366
367
_assignment_transfer(v);
368
369
let narrowed = narrow_to_assigned_value(v, value_type);
370
371
if !_logger.want_hint_for(location) then
372
return narrowed;
373
fi
374
375
let reason = "{v.name} is reassigned";
376
377
if narrowed? then
378
if killed_narrow? /\ !killed_narrow.matches(narrowed) then
379
_logger.inlay(
380
location,
381
"narrowing-assign",
382
"◄►",
383
"{v.name}\n{INLAY_TYPE.render(killed_narrow)}\n{INLAY_TYPE.render(narrowed)}\n\n{reason}");
384
else
385
_logger.inlay(location, "narrowing-assign", "►", INLAY_TYPE.render(narrowed));
386
fi
387
388
return narrowed;
389
fi
390
391
let declared = declared_type_of(v);
392
393
if killed_narrow? then
394
_logger.inlay(location, "narrowing-killed", "◄", "{v.name}\n{declared}\n\n{reason}");
395
elif killed_presence then
396
// The presence bit is redundant with a non-optional
397
// declared type — a non-optional variable is always
398
// known to hold a value, so the bit adds nothing over
399
// the declaration and the kill hint would confuse the
400
// reader by pointing at a narrowing that never carried
401
// information.
402
if declared? /\ declared.is_optional then
403
_logger.inlay(location, "narrowing-killed", "◄", "{v.name}\n{declared}\n\n{reason}");
404
fi
405
fi
406
407
return null;
408
si
409
410
// The assignment transfer proper: drop every fact the write
411
// invalidates and restore the declared view.
412
_assignment_transfer(v: Symbol) is
413
_mark_literal_impure();
414
415
_current_env.drop_non_null(v);
416
417
// Reassigning the root redirects every `v.…` path; and a
418
// bare field target writes `self.<v>`, so any path reading
419
// through that field — on any receiver — is stale too.
420
_current_env.drop_paths_rooted_at(v);
421
422
if isa Semantic.Symbols.Field(v) then
423
_current_env.drop_paths_through(v);
424
fi
425
426
if !_current_env.contains(v) then
427
return;
428
fi
429
430
_current_env.drop_narrow(v);
431
432
let declared = declared_type_of(v);
433
434
if declared? then
435
_set_symbol_type(v, declared);
436
fi
437
si
438
439
// Narrow `v` to the static type of a just-assigned value when
440
// that type is more specific than the declared type, so
441
// `if isa CAT(pet) then pet = DOG()` leaves `pet` viewed as DOG
442
// rather than reverting all the way to the declared Animal. The
443
// assigned value always typechecks against the declared type, so
444
// this only ever tightens, never relaxes. Runs after the
445
// assignment transfer has reset the target to its declared
446
// type. Returns the narrowed-to view when a narrow was
447
// applied, else null.
448
//
449
// Locals only (locals and parameters): a field target wants the
450
// path-narrow plumbing (`self.field` is an access path, and its
451
// facts die at calls), not a symbol narrow.
452
//
453
// A null RHS contributes no type information — `x = null` is
454
// fully described by the presence facts the assignment transfer
455
// already maintains.
456
//
457
// A value-type RHS never narrows: storing a value type into a
458
// wider slot boxes it, so the slot holds a box reference and a
459
// bare-struct view would misdescribe every subsequent load —
460
// plain reads carry no unbox projection. (A value-type slot
461
// can't be narrowed anyway: structs have no subtypes.)
462
//
463
// Skipped on an unreachable edge: a bottom environment ignores
464
// `set_narrow`, and applying the symbol-type half without the
465
// environment record would leave a stale narrow no later
466
// restore point knows about.
467
narrow_to_assigned_value(v: Symbol, value_type: Type?) -> Type? is
468
if
469
!value_type? \/
470
value_type.is_null \/
471
value_type.is_value_type \/
472
!v.is_local \/
473
_current_env.is_bottom
474
then
475
return null;
476
fi
477
478
// Only a strict refinement narrows. A mutually-assignable
479
// RHS type is a different spelling of the same slot, not
480
// extra information.
481
let v_type = v.type;
482
483
if v_type? /\ value_type.is_assignable_from(v_type) then
484
return null;
485
fi
486
487
// Tuple element names sit outside the subtype lattice: an
488
// unnamed tuple is assignable where a named-element tuple
489
// is expected, at any nesting depth (an unnamed-tuple array
490
// refines an Iterable of named tuples), so a view that
491
// mentions a tuple can silently lose the declared names and
492
// break by-name element access. Keep the declared spelling.
493
if _mentions_tuple(value_type, 0) then
494
return null;
495
fi
496
497
if _apply_one(v, value_type) then
498
_current_env.set_narrow(v, value_type);
499
return v.type;
500
fi
501
502
return null;
503
si
504
505
// True when `t` is or mentions a tuple type anywhere in its
506
// type arguments. Depth-capped: type arguments cannot cycle,
507
// but the cap keeps a malformed recursive shape from spinning.
508
_mentions_tuple(t: Type?, depth: int) -> bool is
509
if !t? \/ depth > 8 then
510
return false;
511
fi
512
513
// Tuple types appear in three shapes: the ghūl-side
514
// Types.TUPLE, and the reflected ValueTuple wrappers,
515
// which carry `is_value_tuple` / `tuple_element_names`
516
// but are not TUPLE subclasses.
517
if isa Semantic.Types.TUPLE(t) \/ t.is_value_tuple \/ t.tuple_element_names? then
518
return true;
519
fi
520
521
for a in t.arguments do
522
if _mentions_tuple(a, depth + 1) then
523
return true;
524
fi
525
od
526
527
return false;
528
si
529
530
// Call transfer: a field is plain storage a callee can reach
531
// through `this` and reassign, so a narrow or presence fact
532
// proven before a call cannot be trusted after it, and a
533
// property's getter reads arbitrary heap the callee may have
534
// written. Locals are unreachable to the callee, so their
535
// facts survive. Fires once a call expression has been
536
// compiled — its receiver has already been read, so the
537
// call's own narrowed access is unaffected.
538
on_call(location: LOCATION) is
539
_heap_epoch = _heap_epoch + 1;
540
_mark_literal_impure();
541
542
let stale = Collections.SET[Symbol]();
543
544
for v in _current_env.variables do
545
if isa Semantic.Symbols.Field(v) \/ isa Semantic.Symbols.Property(v) then
546
stale.add(v);
547
fi
548
od
549
550
for v in _current_env.non_null_variables do
551
if isa Semantic.Symbols.Field(v) \/ isa Semantic.Symbols.Property(v) then
552
stale.add(v);
553
fi
554
od
555
556
for v in stale do
557
if !_shield.mark_if_shielded(v) then
558
_report_kill(v, location, "a call here may change it");
559
forget(v);
560
fi
561
od
562
563
// Every path fact dies here: a hop's own getter is
564
// store-free by construction, but the callee may have
565
// stored to anything the path reads through.
566
_report_dropped_paths(location, "a call here may change it", false);
567
_current_env.drop_all_paths();
568
si
569
570
// Emit a kill hint for each tracked path about to be dropped,
571
// deduplicating a path carrying both a narrow and a presence
572
// fact. When `getter_only`, restricts to getter-bearing paths —
573
// the subset a heap store discards. Gated once on the call
574
// location so the set is not built during a normal compile.
575
_report_dropped_paths(location: LOCATION, reason: string, getter_only: bool) is
576
if !_logger.want_hint_for(location) then
577
return;
578
fi
579
580
let doomed = Collections.SET[ACCESS_PATH]();
581
582
for p in _current_env.narrowed_paths do
583
if !getter_only \/ p.has_getter_hop then
584
doomed.add(p);
585
fi
586
od
587
588
for p in _current_env.non_null_paths do
589
if !getter_only \/ p.has_getter_hop then
590
doomed.add(p);
591
fi
592
od
593
594
for p in doomed do
595
_report_path_kill(p, location, reason);
596
od
597
si
598
599
// Heap-store transfer: a direct store — to a field, a
600
// property, an index or any member path — can change what any
601
// property getter returns, so property facts cannot survive
602
// it. Field facts do: a store to one field cannot alter
603
// another field, and a store to the narrowed field itself is
604
// handled by the assignment transfer. Local facts are
605
// unaffected.
606
on_heap_store(location: LOCATION) is
607
_heap_epoch = _heap_epoch + 1;
608
_mark_literal_impure();
609
610
let stale = Collections.SET[Symbol]();
611
612
for v in _current_env.variables do
613
if isa Semantic.Symbols.Property(v) then
614
stale.add(v);
615
fi
616
od
617
618
for v in _current_env.non_null_variables do
619
if isa Semantic.Symbols.Property(v) then
620
stale.add(v);
621
fi
622
od
623
624
for v in stale do
625
_report_kill(v, location, "a store here may change it");
626
forget(v);
627
od
628
629
_report_dropped_paths(location, "a store here may change it", true);
630
_current_env.drop_getter_paths();
631
si
632
633
// Member-store transfer: a store through `receiver.member`
634
// invalidates the member's own facts and every path reading
635
// through it. Keyed on the member symbol, not the receiver —
636
// the written receiver may alias whatever receiver a fact was
637
// established on, so `other.f = e` must invalidate a fact
638
// proven on `self`'s bare `f` just as `f = e` would.
639
on_member_store(member: Symbol?) is
640
if !member? then
641
return;
642
fi
643
644
_heap_epoch = _heap_epoch + 1;
645
_mark_literal_impure();
646
647
forget(member);
648
649
_current_env.drop_paths_rooted_at(member);
650
_current_env.drop_paths_through(member);
651
si
652
653
// Drop a single field's narrow + presence facts — the call
654
// transfer applied to one variable.
655
forget(v: Symbol) is
656
if _current_env.contains(v) then
657
_current_env.drop_narrow(v);
658
659
let declared = declared_type_of(v);
660
661
if declared? then
662
_set_symbol_type(v, declared);
663
fi
664
fi
665
666
_current_env.drop_non_null(v);
667
si
668
669
// Exempt `v` from the call transfer until its frame is released.
670
// See RECEIVER_SHIELD.push.
671
push_shield(v: Symbol?) -> SHIELD_FRAME? => _shield.push(v);
672
673
// Release a frame from `push_shield`, returning whether a call
674
// dropped the receiver. See RECEIVER_SHIELD.release.
675
release_shield(frame: SHIELD_FRAME?) -> bool => _shield.release(frame);
676
677
// Register `v` as a deferred-init local subject to the
678
// definite-assignment use-before-assignment check.
679
track_deferred(v: Symbol) is
680
_tracked.add(v);
681
si
682
683
// True iff `v` is a tracked deferred-init local.
684
is_tracked(v: Symbol) -> bool => _tracked.contains(v);
685
686
// True iff `v` is definitely assigned at the current point.
687
is_assigned(v: Symbol) -> bool => _current_env.is_assigned(v);
688
689
// Record `v` as definitely assigned at the current point.
690
mark_assigned(v: Symbol) is
691
_current_env.set_assigned(v);
692
si
693
694
// True iff `v` is known to hold a value at the current point.
695
is_non_null(v: Symbol) -> bool => _current_env.is_non_null(v);
696
697
// Record `v` as known to hold a value at the current point —
698
// used by the `x!` (unwrap) transfer.
699
mark_non_null(v: Symbol) is
700
_current_env.set_non_null(v);
701
si
702
703
// Editor-only narrowing-introduction hint emitted at a site
704
// outside the condition analyzer (unwrap `x!`, non-optional
705
// initializer / assignment). Same open-files gating as
706
// `_report_kill`; each caller supplies its own slug so the
707
// editor can suppress it independently. `detail` carries only the
708
// narrowed-to type; NARROWING_INLAY_MERGER builds the hover text.
709
report_narrowing_site(location: LOCATION, code: string, label: string, detail: string) is
710
if !_logger.want_hint_for(location) then
711
return;
712
fi
713
714
_logger.inlay(location, code, label, detail);
715
si
716
717
// True iff the member-access `path` is known to hold a value
718
// at the current point — consulted at member-load sites to
719
// narrow the access optional -> non-optional.
720
is_non_null_path(path: ACCESS_PATH?) -> bool => path? /\ _current_env.is_non_null_path(path);
721
722
// The type recorded for `path` at the current point, or null
723
// when none — consulted at member-load sites to narrow the
724
// access to its recorded static subtype.
725
narrowed_type_of_path(path: ACCESS_PATH?) -> Type? =>
726
if path? then _current_env.narrowed_type_of_path(path) else null fi;
727
728
// Compose a loaded type with the recorded path narrow into a
729
// sound view type, or null when the narrow cannot be applied.
730
// The rules mirror `_apply_one` for symbol narrowing:
731
// - reject error / inferred targets
732
// - strict-subtype narrow -> target
733
// - sibling / class+trait -> INTERSECTION(loaded, target)
734
// - reject supertype broadening
735
// Returns null when the composed view equals the loaded type
736
// itself, so callers can skip the wrap in that case.
737
compose_path_narrow(loaded: Type?, target: Type?) -> Type? is
738
if !loaded? \/ !target? then
739
return null;
740
fi
741
742
if target.is_error \/ target.is_inferred then
743
return null;
744
fi
745
746
let effective_target: Type mut = target;
747
748
if !loaded.is_optional /\ target.is_optional then
749
let stripped = target.as_non_optional();
750
effective_target = stripped;
751
fi
752
753
if loaded.matches(effective_target) then
754
return null;
755
fi
756
757
// A bounded type variable narrows through its bound, the same
758
// way `_apply_one` and member access resolve through it.
759
let loaded_effective =
760
if let bound = loaded.bound_type then bound else loaded fi;
761
762
if loaded_effective.is_assignable_from(effective_target) then
763
return effective_target;
764
fi
765
766
let both_reference = !loaded_effective.is_value_type /\ !effective_target.is_value_type;
767
let loaded_supertypes_target = effective_target.is_assignable_from(loaded_effective);
768
769
if !both_reference \/ loaded_supertypes_target then
770
return null;
771
fi
772
773
let composed = INTERSECTION.try_create(loaded_effective, effective_target);
774
775
if !composed? \/ composed.matches(loaded_effective) then
776
return null;
777
fi
778
779
return composed;
780
si
781
782
// Apply one narrow if it is sound. Two cases:
783
//
784
// - Strict-subtype narrow: `t` is a static subtype of v's
785
// declared type. Narrow v.type to t directly.
786
//
787
// - Sibling/class+trait narrow: neither t nor declared
788
// subtypes the other, both are reference types. The
789
// runtime `isa` check guarantees the value satisfies
790
// both — narrow v.type to an INTERSECTION of declared
791
// and t so subsequent member lookups can see both
792
// sides. (For pure trait→trait or class→trait
793
// narrowings the user's earlier "drops the declared
794
// side" trade-off becomes a non-issue.)
795
//
796
// Supertype broadening is still rejected — narrowing must
797
// be sound and informative; widening declared → t where t
798
// supertypes declared would lose information.
799
//
800
// Captures the declared type on first narrow. Returns true
801
// when the narrow was applied.
802
_apply_one(v: Symbol, t: Type) -> bool is
803
if !v.type? then
804
return false;
805
fi
806
807
if t.is_error \/ t.is_inferred then
808
return false;
809
fi
810
811
let current = v.type!;
812
let declared = declared_type_of(v);
813
814
// Strict non-nullable-by-default: a non-optional slot
815
// can't be narrowed to an optional type. If `current`
816
// is already non-optional (the if-X? check has fired)
817
// and the narrowing target is optional, strip the
818
// optional layer — the narrow is from a non-null value
819
// to a more specific type, never re-introducing
820
// optionality.
821
let target: Type mut = t;
822
if !current.is_optional /\ target.is_optional then
823
let stripped = target.as_non_optional();
824
target = stripped;
825
fi
826
827
if !declared? \/ current.matches(target) then
828
return false;
829
fi
830
831
// A bounded type variable narrows through its bound: a value
832
// of `T: List[E]` can be a `CONS[E]` at runtime even though
833
// `CONS[E]` subtypes the bound, not `T` itself. Evaluate the
834
// narrow against the bound (what `T` is), not the variable.
835
// The narrowed view stays a valid subtype; IL loads it against
836
// the variable's declared `!!N` with a checked cast.
837
let current_effective =
838
if let bound = current.bound_type then bound else current fi;
839
840
let narrowed: Type? mut = null;
841
842
if current_effective.is_assignable_from(target) then
843
// Strict-subtype narrow: target is a static subtype of
844
// the current (possibly already-narrowed) type.
845
narrowed = target;
846
else
847
// Sibling / class+trait relaxation: extend current
848
// with target. Factory drops redundant supertypes and
849
// collapses to a plain type if a single survivor
850
// remains. Composes correctly with an already-
851
// narrowed intersection — adding a second trait
852
// produces a three-element intersection rather
853
// than replacing.
854
let both_reference = !current_effective.is_value_type /\ !target.is_value_type;
855
let current_supertypes_t = target.is_assignable_from(current_effective);
856
857
if !both_reference \/ current_supertypes_t then
858
return false;
859
fi
860
861
narrowed = INTERSECTION.try_create(current_effective, target);
862
863
if !narrowed? then
864
// Unrelated concrete identities — no value can be
865
// both, so there is no narrowed view to apply.
866
return false;
867
fi
868
869
if narrowed.matches(current) then
870
return false;
871
fi
872
fi
873
874
if !_declared.contains_key(v) then
875
_declared[v] = current;
876
fi
877
878
_set_symbol_type(v, narrowed);
879
880
return true;
881
si
882
si
883
884
// RAII narrowing-env speculation, mirroring the logger's
885
// LOGGER_SPECULATE_THEN_* disposables. Construct to snapshot the
886
// current env as a baseline; a retry re-walk resets to it through
887
// `_flow.restore()`; on scope exit COMMIT keeps the walked facts,
888
// ROLL_BACK discards them. Pair the flow disposable with the logger
889
// disposable at any site that speculatively re-walks expressions, so
890
// the narrowing facts and the diagnostics roll back together.
891
struct FLOW_SPECULATE_THEN_COMMIT: Disposable is
892
_flow: NARROWING_FLOW?;
893
894
init(flow: NARROWING_FLOW) is
895
_flow = flow;
896
flow.speculate();
897
si
898
899
commit() is
900
_flow!.commit();
901
_flow = null;
902
si
903
904
cancel() is
905
_flow = null;
906
si
907
908
dispose() is
909
if _flow? then
910
_flow.commit();
911
_flow = null;
912
fi
913
si
914
si
915
916
struct FLOW_SPECULATE_THEN_ROLL_BACK: Disposable is
917
_flow: NARROWING_FLOW?;
918
919
init(flow: NARROWING_FLOW) is
920
_flow = flow;
921
flow.speculate();
922
si
923
924
roll_back() is
925
_flow!.roll_back();
926
_flow = null;
927
si
928
929
cancel() is
930
_flow = null;
931
si
932
933
dispose() is
934
if _flow? then
935
_flow.roll_back();
936
_flow = null;
937
fi
938
si
939
si
940
si