Skip to content
← Back

src/syntax/process/narrowing/condition_analysis.ghul

1
namespace Syntax.Process is
2
use Logging.Logger
3
use Source.LOCATION
4
use Semantic.Types.Type
5
6
// The then/else narrowing environments a boolean condition
7
// implies: `then_env` holds while the condition is true,
8
// `else_env` while it is false.
9
class CONDITION_FACTS is
10
then_env: NARROW_ENV public
11
else_env: NARROW_ENV public
12
13
init(then_env: NARROW_ENV, else_env: NARROW_ENV) is
14
self.then_env = then_env
15
self.else_env = else_env
16
si
17
si
18
19
// Pure analysis of a *walked* boolean condition AST: given the
20
// environment in force before the condition, produce the
21
// environments in force along its true and false edges.
22
//
23
// `isa T(x)` leaves contribute a type narrow on their target; an
24
// `x?` (has-value) leaf contributes a presence fact (`x` known to
25
// hold a value on the true edge); `/\` `\/` `!` compose the
26
// leaves; anything else is opaque (no narrowing either way).
27
// Narrowing targets are resolved through the `resolve_target`
28
// delegate supplied at construction — in the compiler it is the
29
// visitor's scope-aware lookup; in tests it is a stub.
30
class CONDITION_ANALYZER is
31
_resolve_target: (Trees.Expressions.Expression) -> Semantic.Symbols.Symbol?
32
_resolve_path: (Trees.Expressions.Expression) -> ACCESS_PATH?
33
34
// The crossing-log count recorded when a condition-leaf test
35
// expression was walked, or null when no walk stamped it. The
36
// facts a leaf forms are stamped with it so a later adoption
37
// skips the condition's calls that ran before the leaf's own
38
// test — see NARROWING_FLOW.note_test_site.
39
_test_site_mark: (Trees.Expressions.Expression) -> int?
40
41
// Editor-only sink for narrowing-site hints. When set,
42
// each narrowing introduction (`x?`, `isa T(x)`, `if let ...`,
43
// etc.) is announced through the logger carrying its per-site
44
// slug so the editor can render — and independently suppress —
45
// each kind. Null in the pure-analysis tests, which exercise
46
// the environments directly and don't need the diagnostics.
47
// Only `want_hint_for` and `hint` are consulted.
48
_logger: Logger?
49
50
// Depth counter for facts-only recursion. `pre_binary` in
51
// compile_operators pre-analyses the left of `/\` / `\/` to
52
// thread the right's narrowing environment; the enclosing
53
// condition context (if / while / assert) then analyses the
54
// whole condition. Without suppression the same narrowing
55
// site emits an inlay on each pass. Non-zero while a caller
56
// is inside `analyze_condition_facts_only`.
57
_silent_depth: int
58
59
init(
60
resolve_target: (Trees.Expressions.Expression) -> Semantic.Symbols.Symbol?,
61
resolve_path: (Trees.Expressions.Expression) -> ACCESS_PATH?,
62
test_site_mark: (Trees.Expressions.Expression) -> int?
63
) is
64
_resolve_target = resolve_target
65
_resolve_path = resolve_path
66
_test_site_mark = test_site_mark
67
_declined_narrowing_sites = Collections.SET[LOCATION]()
68
si
69
70
init(
71
resolve_target: (Trees.Expressions.Expression) -> Semantic.Symbols.Symbol?,
72
resolve_path: (Trees.Expressions.Expression) -> ACCESS_PATH?,
73
test_site_mark: (Trees.Expressions.Expression) -> int?,
74
logger: Logger
75
) is
76
_resolve_target = resolve_target
77
_resolve_path = resolve_path
78
_declined_narrowing_sites = Collections.SET[LOCATION]()
79
_test_site_mark = test_site_mark
80
_logger = logger
81
si
82
83
// The creation index a leaf's facts are stamped with: the log
84
// count the walk recorded at the leaf, or zero when the leaf
85
// was never walked — which keeps the attach-everything
86
// behaviour adoption always had.
87
_leaf_mark(expr: Trees.Expressions.Expression) -> int is
88
if let mark = _test_site_mark(expr) then
89
return mark
90
fi
91
92
return 0
93
si
94
95
// Record a narrowing-introduction inlay. Editor-only: gated on
96
// analysis mode and the client's open-files set so a narrowing in
97
// a file the user is not viewing costs nothing. `label` is the
98
// terse ghost text (a single-glyph direction sigil); `detail`
99
// carries only the narrowed-to type. NARROWING_INLAY_MERGER
100
// assembles the hover sentence at read-out, folding a true/false
101
// edge pair (`narrowing-<kind>` + `narrowing-<kind>-complement`)
102
// at one location into a single hint.
103
_inlay(location: LOCATION, code: string, label: string, detail: string) is
104
let logger = _logger
105
106
if _silent_depth > 0 \/ !logger? \/ !logger.want_hint_for(location) \/ _declined_narrowing_sites.contains(location) then
107
return
108
fi
109
110
logger.inlay(location, code, label, detail)
111
si
112
113
// The editor surface for a fact the solved relations cannot
114
// back: a narrowing read through a getter whose own re-read
115
// the solve cannot discharge is dead at its first use, so the
116
// site presents no narrowing - the introduction inlay is
117
// withheld and this hint says why. The fact itself is
118
// declined in NARROW_ENV._declines_getter_fact, which also
119
// records the kill that re-walks the body should a later
120
// solve back the getter. Facts on fields and locals never
121
// read through a getter and are always backed.
122
_decline_unbacked_narrowing(
123
location: LOCATION,
124
target: Semantic.Symbols.Symbol?,
125
path: ACCESS_PATH?,
126
presence: bool,
127
type_narrow: bool
128
) is
129
if !KILL_LEDGER.can_kill_in_walk then
130
return
131
fi
132
133
let through_getter =
134
if target? then
135
isa Semantic.Symbols.Property(target)
136
elif path? then
137
path.has_getter_hop
138
else
139
false
140
fi
141
142
if !through_getter then
143
return
144
fi
145
146
// The type question is the stricter one - a monotone
147
// memoiser passes the presence question and fails it -
148
// so when the type fact is unbacked the strict question
149
// is the one that names the getter.
150
let unproven =
151
RELIANCES.first_unproven_getter(target, path, if type_narrow then false else true fi)
152
153
if !unproven? then
154
return
155
fi
156
157
if _declined_narrowing_sites.contains(location) then
158
return
159
fi
160
161
_declined_narrowing_sites.add(location)
162
163
let logger = _logger
164
165
if _silent_depth > 0 \/ !logger? \/ !logger.want_hint_for(location) then
166
return
167
fi
168
logger.hint(
169
location,
170
"narrowing-unavailable",
171
"cannot narrow through '{unproven.name}': its getter is not provably stable; use 'if let' or copy the value into a local variable",
172
unproven.location,
173
"getter declared here")
174
si
175
176
// Whether the site withheld its introduction inlay because
177
// the relations cannot back the fact: consulted by _inlay.
178
_declined_narrowing_sites: Collections.SET[LOCATION]
179
180
// Compute a condition's true/false narrowing envs WITHOUT
181
// emitting narrowing-introduction inlays for sites walked. Used
182
// by compile_operators.pre_binary to thread the right operand's
183
// env; the enclosing if / while / assert then calls
184
// `analyze_condition` on the whole condition, which is the one
185
// walk that emits.
186
analyze_condition_facts_only(
187
cond: Trees.Expressions.Expression,
188
in_env: NARROW_ENV
189
) -> CONDITION_FACTS is
190
_silent_depth = _silent_depth + 1
191
try
192
return analyze_condition(cond, in_env)
193
finally
194
_silent_depth = _silent_depth - 1
195
yrt
196
si
197
198
// True iff a presence-fact hint for `target` would carry
199
// information — i.e. `target`'s current observed type is
200
// still optional. When it isn't, `target` is already known
201
// to hold a value and the hint would only be noise. Safe
202
// for the presence hints (`x?` / `x != null`) because their
203
// fact is a set-add, not a compose-against-existing.
204
_presence_hint_would_add(target: Semantic.Symbols.Symbol) -> bool =>
205
let t = target.type in t? /\ t.is_optional
206
207
// Drill `type` to the underlying Classy (peeling Symbols.GENERIC
208
// wrapping for specialized generics, and peeling INTERSECTION
209
// for stacked-narrow types like `Declared & Variant[T]`). Null
210
// if `type` isn't a NAMED of a Classy.
211
get_classy_for_narrowing(type: Type?) -> Semantic.Symbols.Classy? is
212
if !type? then
213
return null
214
fi
215
216
if isa Semantic.Types.INTERSECTION(type) then
217
for m in type.members do
218
let inner = get_classy_for_narrowing(m)
219
if inner? then
220
return inner
221
fi
222
od
223
return null
224
fi
225
226
if !isa Semantic.Types.NAMED(type) then
227
return null
228
fi
229
230
// A bounded type variable narrows through its bound: `T`
231
// constrained to a union resolves variants of that union.
232
if type.is_type_variable then
233
return get_classy_for_narrowing(type.bound_type)
234
fi
235
236
let symbol mut = type.symbol
237
238
if isa Semantic.Symbols.GENERIC(symbol) then
239
symbol = symbol.symbol
240
fi
241
242
if !isa Semantic.Symbols.Classy(symbol) then
243
return null
244
fi
245
246
return symbol
247
si
248
249
// Pick the type that carries the receiver's generic args for
250
// variant-type construction. ONE_OF holds its original union
251
// NAMED/GENERIC as `underlying_type`; INTERSECTION returns the
252
// first member that yields a usable underlying (a NAMED whose
253
// symbol is a union or variant); any other NAMED can be used
254
// directly.
255
pick_underlying_type(receiver_type: Type?) -> Semantic.Types.NAMED? is
256
if !receiver_type? then
257
return null
258
fi
259
260
if isa Semantic.Types.ONE_OF(receiver_type) then
261
return receiver_type.underlying_type
262
fi
263
264
if isa Semantic.Types.INTERSECTION(receiver_type) then
265
for m in receiver_type.members do
266
let inner = pick_underlying_type(m)
267
if inner? /\ get_classy_for_narrowing(inner)? then
268
return inner
269
fi
270
od
271
return null
272
fi
273
274
if isa Semantic.Types.NAMED(receiver_type) then
275
// A bounded type variable carries the variant's generic
276
// args through its bound (`T: List[E]` supplies `E`).
277
if receiver_type.is_type_variable then
278
return pick_underlying_type(receiver_type.bound_type)
279
fi
280
281
return receiver_type
282
fi
283
284
return null
285
si
286
287
// If `target_type` names a variant of `receiver_type`'s
288
// union, return the variant specialized with the receiver's
289
// generic args; otherwise return `target_type` unchanged.
290
// Lets `isa V(x)` / `cast V(x)` resolve a bare variant of a
291
// generic union (`isa CONS(l)` for `l: List[int]`) to its
292
// closed-generic form so IL emission references a loadable
293
// type — the open generic class itself isn't a valid
294
// operand for `isinst` / `castclass`. The specialized type is
295
// built fresh from the receiver's generic args and is never
296
// itself optional, so a target written `V?` has its
297
// optionality re-applied here — otherwise a written
298
// `cast V?(x)` would silently narrow to non-optional `V` and
299
// draw a false `cast-may-throw`.
300
specialize_variant_for_receiver(receiver_type: Type?, target_type: Type?) -> Type? is
301
if !target_type? \/ !receiver_type? then
302
return target_type
303
fi
304
305
let classy = get_classy_for_narrowing(target_type)
306
307
if !classy? \/ !classy.is_variant then
308
return target_type
309
fi
310
311
return
312
let specialized = try_get_variant_type_for_classy(receiver_type, classy) in
313
if specialized? then
314
if target_type.is_optional then specialized.as_optional() else specialized fi
315
else
316
target_type
317
fi
318
si
319
320
// For an `isa V(x)` check where `V` is a variant of the
321
// receiver's union, return the variant type with the same
322
// generic args as the receiver. Null when not applicable.
323
// Receiver shapes covered: the wide union, a ONE_OF over it,
324
// or an already-narrowed singleton variant of it. Variants
325
// share their generic parameters with the union, so a
326
// variant-NAMED receiver carries the same arg list and the
327
// singleton-build below picks them up unchanged.
328
try_get_variant_type_for_classy(
329
receiver_type: Type,
330
variant: Semantic.Symbols.Classy
331
) -> Type? is
332
if !variant.is_variant then
333
return null
334
fi
335
336
let receiver_classy = get_classy_for_narrowing(receiver_type)
337
338
if !receiver_classy? then
339
return null
340
fi
341
342
// Resolve the receiver's union — directly when the
343
// receiver is the union, or the variant's owner when the
344
// receiver is itself a variant of one.
345
let union_classy: Semantic.Symbols.Classy? mut = null
346
347
if receiver_classy.is_union then
348
union_classy = receiver_classy
349
elif receiver_classy.is_variant then
350
if let owner: Semantic.Symbols.Classy = receiver_classy.owner then
351
union_classy = owner
352
fi
353
fi
354
355
if !union_classy? \/ !union_classy.is_union then
356
return null
357
fi
358
359
// Verify `variant` actually belongs to that union.
360
let is_member mut = false
361
for s in union_classy.symbols do
362
if s == variant then
363
is_member = true
364
fi
365
od
366
367
if !is_member then
368
return null
369
fi
370
371
// No in-set check here: IL emission needs the
372
// specialized variant type even when the runtime test is
373
// statically false, because a bare open-generic variant
374
// class is not loadable by the CLR. Narrowing-soundness
375
// checks (e.g. ONE_OF.contains_subtype) belong at the
376
// call site that consumes this as a narrow target.
377
let underlying = pick_underlying_type(receiver_type)
378
379
if !underlying? then
380
return null
381
fi
382
383
return Semantic.Types.ONE_OF.build_singleton_subtype(underlying, variant)
384
si
385
386
// Build the complement narrowing — the in-set members of
387
// `receiver_type` that don't appear in `eliminated`. When
388
// `receiver_type` is itself a ONE_OF the in-set is its
389
// narrowed members, not the full root; otherwise it is
390
// every alternative of the closed root (variants of a
391
// union, direct subclasses of a closed class). Returns a
392
// singleton type, a ONE_OF, or null when the chain exhausts
393
// the in-set or the receiver isn't a closed root.
394
try_get_complement_after_eliminated(
395
receiver_type: Type,
396
eliminated: Collections.Iterable[Semantic.Symbols.Classy]
397
) -> Type? is
398
// The complement of an optional receiver is optional:
399
// the eliminating test also fails for null, so the
400
// complement edge keeps the null case. Compute over the
401
// stripped type and re-flag the result. Callers that can
402
// prove the value present strip the flag back off.
403
let is_receiver_optional = receiver_type.is_optional
404
let receiver = receiver_type.as_non_optional()
405
406
let classy = get_classy_for_narrowing(receiver)
407
408
if !classy? \/ !classy.is_closed_root then
409
return null
410
fi
411
412
// Closed classes with generic receivers need extra
413
// specialisation work — subclasses don't always share
414
// the base's type parameter list. Variants share their
415
// owner union's argument_names slot-for-slot by language
416
// design, so unions don't hit this constraint.
417
if classy.is_class /\ isa Semantic.Types.GENERIC(receiver) then
418
return null
419
fi
420
421
// The universe is the closed root's in-set, optionally
422
// including the root itself. For unions the root never
423
// appears at runtime (only its variants do). For closed
424
// classes the root joins the universe when concrete —
425
// otherwise a caller could construct a bare root
426
// instance and the singleton-collapse narrow would
427
// produce an unsound method resolution.
428
let universe = Collections.LIST[Semantic.Symbols.Classy]()
429
430
if classy.is_class /\ !classy.is_abstract then
431
universe.add(classy)
432
fi
433
434
for s in classy.closed_alternatives do
435
universe.add(s)
436
od
437
438
let one_of = cast Semantic.Types.ONE_OF?(receiver)
439
440
let complement = Collections.LIST[Semantic.Symbols.Classy]()
441
442
for member in universe do
443
if one_of? /\ !one_of.contains_subtype(member) then
444
continue
445
fi
446
447
let is_eliminated mut = false
448
449
for e in eliminated do
450
if e == member then
451
is_eliminated = true
452
fi
453
od
454
455
if !is_eliminated then
456
complement.add(member)
457
fi
458
od
459
460
let underlying = pick_underlying_type(receiver)
461
462
if !underlying? then
463
return null
464
fi
465
466
let result = Semantic.Types.ONE_OF.create(underlying, complement)
467
468
if result? /\ is_receiver_optional then
469
return result.as_optional()
470
fi
471
472
return result
473
si
474
475
// Narrow target for `isa C(x)` where C is a direct subclass
476
// of `x`'s closed root class. Null when not applicable —
477
// receiver is open / imported / not the right root, target
478
// isn't a direct subclass, or the receiver is generic (the
479
// generic case needs a parameter-flow rule we don't have
480
// yet).
481
try_get_closed_subclass_narrow_type(
482
receiver_type: Type,
483
target_classy: Semantic.Symbols.Classy
484
) -> Type? is
485
if !target_classy.is_class then
486
return null
487
fi
488
489
let receiver_classy = get_classy_for_narrowing(receiver_type)
490
491
if !receiver_classy? \/ receiver_classy.is_open then
492
return null
493
fi
494
495
if !receiver_classy.is_class then
496
return null
497
fi
498
499
if isa Semantic.Types.GENERIC(receiver_type) then
500
return null
501
fi
502
503
// Verify target_classy is in receiver's closed in-set:
504
// walking the ONE_OF when one is in play, otherwise the
505
// root's direct subclasses.
506
let one_of: Semantic.Types.ONE_OF? mut = null
507
if isa Semantic.Types.ONE_OF(receiver_type) then
508
one_of = receiver_type
509
fi
510
511
if one_of? then
512
if !one_of.contains_subtype(target_classy) then
513
return null
514
fi
515
else
516
let is_member mut = false
517
for s in receiver_classy.closed_subclasses do
518
if s == target_classy then
519
is_member = true
520
fi
521
od
522
523
if !is_member then
524
return null
525
fi
526
fi
527
528
return Semantic.Types.NAMED(target_classy)
529
si
530
531
// Analyze a walked boolean condition into its true/false
532
// narrowing environments. Always returns fresh (copied)
533
// environments — callers may mutate them freely.
534
analyze_condition(cond: Trees.Expressions.Expression?, in_env: NARROW_ENV) -> CONDITION_FACTS is
535
if !cond? then
536
return _opaque(cond, in_env)
537
fi
538
539
if isa Trees.Expressions.BINARY(cond) then
540
let binary = cond
541
542
let op = binary.operation.name
543
544
if op =~ "/\\" then
545
let l = analyze_condition(binary.left, in_env)
546
let r = analyze_condition(binary.right, l.then_env)
547
548
return CONDITION_FACTS(
549
r.then_env,
550
NARROW_ENV.join(l.else_env, r.else_env)
551
)
552
elif op =~ "\\/" then
553
let l = analyze_condition(binary.left, in_env)
554
let r = analyze_condition(binary.right, l.else_env)
555
556
return CONDITION_FACTS(
557
NARROW_ENV.join(l.then_env, r.then_env),
558
r.else_env
559
)
560
elif op =~ "==" then
561
return _analyze_null_compare(binary, in_env)
562
fi
563
elif isa Trees.Expressions.UNARY(cond) then
564
let unary = cond
565
566
if unary.operation.name =~ "!" then
567
let inner = analyze_condition(unary.right, in_env)
568
569
// `!` swaps the true and false edges.
570
return CONDITION_FACTS(inner.else_env, inner.then_env)
571
fi
572
elif isa Trees.Expressions.ISA(cond) then
573
return _analyze_isa(cond, in_env)
574
elif isa Trees.Expressions.HAS_VALUE(cond) then
575
return _analyze_has_value(cond, in_env)
576
fi
577
578
return _opaque(cond, in_env)
579
si
580
581
// A condition that discriminates no variable's type — both
582
// edges keep the incoming environment. A `ref` argument the
583
// condition evaluates writes its target, so it counts as a
584
// definite assignment on both edges; short-circuit joins above
585
// then confine that fact to the edges the write actually reached.
586
_opaque(cond: Trees.Expressions.Expression?, in_env: NARROW_ENV) -> CONDITION_FACTS is
587
let then_env = in_env.copy()
588
let else_env = in_env.copy()
589
590
_mark_ref_assignments(cond, then_env, else_env)
591
592
return CONDITION_FACTS(then_env, else_env)
593
si
594
595
// Operators whose right operand is evaluated only for some
596
// values of the left: `/\`, `\/` and the `??` null-coalesce.
597
_is_short_circuit(operation: string) -> bool =>
598
operation =~ "/\\" \/ operation =~ "\\/" \/ operation =~ "??"
599
600
// Mark every variable the expression writes through a `ref`
601
// argument as assigned on both edges. Recurses only through
602
// unconditionally-evaluated positions: a `ref` behind a
603
// short-circuit operand or a lambda body is not guaranteed to
604
// run, so it is left for its own evaluation to record.
605
_mark_ref_assignments(expr: Trees.Expressions.Expression?, then_env: NARROW_ENV, else_env: NARROW_ENV) is
606
if !expr? then
607
return
608
fi
609
610
if isa Trees.Expressions.REFERENCE(expr) then
611
let reference = expr
612
613
// Only a slot the callee writes assigns its target; the
614
// resolved call recorded that on the REFERENCE.
615
if reference.writes_target then
616
let target = _resolve_target(reference.left)
617
618
if target? then
619
then_env.set_assigned(target)
620
else_env.set_assigned(target)
621
fi
622
fi
623
elif isa Trees.Expressions.CALL(expr) then
624
let call = expr
625
626
_mark_ref_assignments(call.function, then_env, else_env)
627
628
for argument in call.arguments.expressions do
629
_mark_ref_assignments(argument, then_env, else_env)
630
od
631
elif isa Trees.Expressions.MEMBER(expr) then
632
_mark_ref_assignments(expr.left, then_env, else_env)
633
elif isa Trees.Expressions.INDEX(expr) then
634
let index = expr
635
636
_mark_ref_assignments(index.left, then_env, else_env)
637
_mark_ref_assignments(index.index, then_env, else_env)
638
elif isa Trees.Expressions.UNARY(expr) then
639
_mark_ref_assignments(expr.right, then_env, else_env)
640
elif isa Trees.Expressions.CAST(expr) then
641
_mark_ref_assignments(expr.right, then_env, else_env)
642
elif isa Trees.Expressions.HAS_VALUE(expr) then
643
_mark_ref_assignments(expr.left, then_env, else_env)
644
elif isa Trees.Expressions.UNWRAP(expr) then
645
_mark_ref_assignments(expr.left, then_env, else_env)
646
elif isa Trees.Expressions.BINARY(expr) then
647
let binary = expr
648
649
_mark_ref_assignments(binary.left, then_env, else_env)
650
651
// The right operand of a short-circuiting operator only
652
// runs for some left values, so a `ref` there is not a
653
// definite write; the left operand always runs.
654
if !_is_short_circuit(binary.operation.name) then
655
_mark_ref_assignments(binary.right, then_env, else_env)
656
fi
657
fi
658
si
659
660
// `x != null` / `x == null` — the comparison narrows the
661
// non-null operand. `!=` puts it on the true edge, `==` on the
662
// false edge. The `==` real operation covers both surface
663
// forms; `actual_operation` distinguishes them.
664
_analyze_null_compare(binary: Trees.Expressions.BINARY, in_env: NARROW_ENV) -> CONDITION_FACTS is
665
let then_env = in_env.copy()
666
let else_env = in_env.copy()
667
668
// Facts this leaf adds were established when its test
669
// ran; calls the condition walked before it cannot have
670
// invalidated them.
671
let leaf_mark = _leaf_mark(binary)
672
then_env.creation_mark = leaf_mark
673
else_env.creation_mark = leaf_mark
674
675
let operand: Trees.Expressions.Expression? mut = null
676
677
if isa Trees.Expressions.NULL(binary.right) then
678
operand = binary.left
679
elif isa Trees.Expressions.NULL(binary.left) then
680
operand = binary.right
681
fi
682
683
if operand? then
684
let target = _resolve_target(operand)
685
let is_not_equal = binary.actual_operation =~ "!="
686
687
if target? then
688
if is_not_equal then
689
then_env.set_non_null(target)
690
else
691
else_env.set_non_null(target)
692
fi
693
694
_decline_unbacked_narrowing(operand.location, target, null, true, false)
695
696
if _presence_hint_would_add(target) then
697
_inlay(
698
operand.location,
699
"narrowing-null-compare",
700
"►",
701
INLAY_TYPE.render(target.type!.as_non_optional())
702
)
703
fi
704
else
705
let path = _resolve_path(operand)
706
707
if path? then
708
if is_not_equal then
709
then_env.set_non_null_path(path)
710
else
711
else_env.set_non_null_path(path)
712
fi
713
714
_decline_unbacked_narrowing(operand.location, null, path, true, false)
715
716
let path_type = operand.value?.type
717
718
_inlay(
719
operand.location,
720
"narrowing-null-compare",
721
"►",
722
if path_type? then INLAY_TYPE.render(path_type.as_non_optional()) else "" fi
723
)
724
fi
725
fi
726
fi
727
728
return CONDITION_FACTS(then_env, else_env)
729
si
730
731
// Apply the cast target as a then-edge narrow on the if-let
732
// scrutinee — mirrors `_analyze_isa`'s then-edge logic so
733
// `if let p: V = e` and `isa V(e)` produce the same flow
734
// facts (and therefore the same downstream inference shape).
735
//
736
// The scrutinee is walked AFTER this hook runs, so the load
737
// picks up the narrowed symbol type. The narrow target is
738
// specialised against the scrutinee's symbol type (via
739
// `_resolve_target`), so a bare variant target becomes its
740
// receiver-specialised form (e.g. `CONS[int]` not `CONS`)
741
// when the receiver type is already known — otherwise it is
742
// left as the unspecialised written form, and a later body-
743
// retry iteration with a settled receiver gets a chance to
744
// re-narrow tighter.
745
apply_refutable_binding_then_narrow(
746
scrutinee: Trees.Expressions.Expression?,
747
narrow_type: Type?,
748
in_env: NARROW_ENV
749
) -> NARROW_ENV is
750
if !scrutinee? \/ !narrow_type? then
751
return in_env
752
fi
753
754
if narrow_type.is_error \/ narrow_type.is_inferred then
755
return in_env
756
fi
757
758
let target = _resolve_target(scrutinee)
759
let target_path: ACCESS_PATH? mut =
760
if !target? then _resolve_path(scrutinee) else null fi
761
762
if !target? /\ !target_path? then
763
return in_env
764
fi
765
766
// Specialise the narrow target against the scrutinee's
767
// current symbol type so a variant target narrows to its
768
// closed-generic form (e.g. `CONS[int]` for an
769
// `l: List[int]` receiver). For a path scrutinee we take
770
// the value's static type from the walked expression.
771
let receiver: Type? mut = null
772
773
if target? /\ target.type? then
774
receiver = target.type
775
776
if isa Semantic.Types.INFERRED_VARIABLE_TYPE(receiver) then
777
let placeholder = cast Semantic.Types.INFERRED_VARIABLE_TYPE(receiver)
778
let resolved = placeholder.origin.try_get_inferred_type()
779
780
if resolved? /\ !resolved.is_sentinel then
781
receiver = resolved
782
fi
783
fi
784
elif !target? /\ scrutinee.value? then
785
receiver = scrutinee.value.type
786
fi
787
788
let specialized_narrow: Type mut = narrow_type
789
790
if receiver? then
791
let specialized =
792
specialize_variant_for_receiver(receiver, narrow_type)
793
794
if specialized? then
795
specialized_narrow = specialized
796
fi
797
fi
798
799
let result = in_env.copy()
800
801
if target? then
802
result.set_non_null(target)
803
result.set_narrow(target, specialized_narrow)
804
805
_decline_unbacked_narrowing(scrutinee.location, target, null, true, true)
806
807
_inlay(
808
scrutinee.location,
809
"narrowing-if-let",
810
"►",
811
INLAY_TYPE.render(specialized_narrow)
812
)
813
else
814
result.set_non_null_path(target_path!)
815
result.set_path_narrow(target_path, specialized_narrow)
816
817
_decline_unbacked_narrowing(scrutinee.location, null, target_path, true, true)
818
819
_inlay(
820
scrutinee.location,
821
"narrowing-if-let",
822
"►",
823
INLAY_TYPE.render(specialized_narrow)
824
)
825
fi
826
827
return result
828
si
829
830
// Apply variant-complement narrowing to the else edge of a
831
// REFUTABLE_BINDING. Mirrors the variant branch of
832
// `_analyze_isa`'s else narrowing.
833
//
834
// The receiver type is passed in because by the time this
835
// runs the scrutinee's symbol type may have been mutated by
836
// the then-edge narrowing to the cast target (the variant
837
// itself, not the union we want to compute the complement
838
// over). The caller captures the receiver before applying
839
// the then-narrow.
840
//
841
// Multi-clause bindings suppress complement narrowing
842
// entirely: the else edge can be reached because clause 0's
843
// test passed but clause N's failed, in which case clause 0's
844
// scrutinee genuinely *is* its narrow target (not the
845
// complement). Same soundness reasoning as the guarded
846
// single-clause case below.
847
apply_refutable_binding_else_narrow(
848
rb: Trees.Statements.REFUTABLE_BINDING,
849
receiver_type: Type?,
850
in_env: NARROW_ENV
851
) -> NARROW_ENV is
852
if !receiver_type? then
853
return in_env
854
fi
855
856
if rb.clauses.count != 1 then
857
return in_env
858
fi
859
860
let clause = rb.clauses[0]
861
862
let narrow_type_expression = clause.narrow_type_expression
863
864
if !narrow_type_expression? \/ !narrow_type_expression.type? then
865
return in_env
866
fi
867
868
// Guarded if-let: the else arm is reached on two paths —
869
// the type test rejected the scrutinee (`e` is not `V`),
870
// OR the test succeeded but the guard was false (`e` is
871
// `V`). Narrowing to the variant complement on the
872
// second path would be unsound. Mirrors how
873
// `analyze_condition` joins the `/\` else edges: `isa V(x)
874
// /\ guard` widens the `x` narrow back on else (line
875
// 272), it doesn't narrow to the complement.
876
if clause.guard? then
877
return in_env
878
fi
879
880
let narrow_type = narrow_type_expression.type
881
let scrutinee = clause.scrutinee
882
883
let target = _resolve_target(scrutinee)
884
let target_path: ACCESS_PATH? mut =
885
if !target? then _resolve_path(scrutinee) else null fi
886
887
if !target? /\ !target_path? then
888
return in_env
889
fi
890
891
let narrow_classy = get_classy_for_narrowing(narrow_type)
892
893
if !narrow_classy? then
894
return in_env
895
fi
896
897
// Variant-of-union and direct-subclass-of-closed-root are
898
// the two closed-domain shapes that can produce a non-empty
899
// else complement.
900
if !narrow_classy.is_variant /\ !narrow_classy.is_class then
901
return in_env
902
fi
903
904
// Compute the complement against the target's CURRENT
905
// narrow when there is one — an earlier arm in the same
906
// if/elif chain may already have eliminated other
907
// alternatives, and the complement is "what's left minus
908
// this arm's target", not "the declared root minus this
909
// arm's target". The declared receiver is the fallback for
910
// the first arm where no narrow yet exists.
911
let effective_receiver: Type mut = receiver_type
912
let current: Type? mut =
913
if target? then in_env.narrowed_type_of(target) else in_env.narrowed_type_of_path(target_path!) fi
914
915
if current? then
916
effective_receiver = current
917
fi
918
919
let one_of: Semantic.Types.ONE_OF? mut = null
920
if isa Semantic.Types.ONE_OF(effective_receiver) then
921
one_of = effective_receiver
922
fi
923
924
if one_of? /\ !one_of.contains_subtype(narrow_classy) then
925
return in_env
926
fi
927
928
let eliminated = Collections.LIST[Semantic.Symbols.Classy]()
929
eliminated.add(narrow_classy)
930
931
let complement mut = try_get_complement_after_eliminated(effective_receiver, eliminated)
932
933
if !complement? then
934
return in_env
935
fi
936
937
// The else edge is null-free when the scrutinee was
938
// already proven to hold a value before the binding.
939
let was_non_null =
940
if target? then in_env.is_non_null(target) else in_env.is_non_null_path(target_path!) fi
941
942
if complement.is_optional /\ was_non_null then
943
complement = complement.as_non_optional()
944
fi
945
946
let result = in_env.copy()
947
948
// The complement is computed against the effective receiver
949
// — the current narrow when an earlier arm already narrowed
950
// the target — so it is the absolute narrowed type for the
951
// else edge and must replace any prior narrow rather than
952
// compose with it.
953
if target? then
954
result.replace_narrow(target, complement)
955
956
_decline_unbacked_narrowing(scrutinee.location, target, null, false, true)
957
958
_inlay(
959
scrutinee.location,
960
"narrowing-if-let-complement",
961
"►",
962
INLAY_TYPE.render(complement)
963
)
964
else
965
result.replace_path_narrow(target_path!, complement)
966
967
_decline_unbacked_narrowing(scrutinee.location, null, target_path, false, true)
968
969
_inlay(
970
scrutinee.location,
971
"narrowing-if-let-complement",
972
"►",
973
INLAY_TYPE.render(complement)
974
)
975
fi
976
977
return result
978
si
979
980
981
_analyze_isa(`isa: Trees.Expressions.ISA, in_env: NARROW_ENV) -> CONDITION_FACTS is
982
let then_env = in_env.copy()
983
let else_env = in_env.copy()
984
985
// Facts this leaf adds were established when its test
986
// ran; calls the condition walked before it cannot have
987
// invalidated them.
988
let leaf_mark = _leaf_mark(`isa)
989
then_env.creation_mark = leaf_mark
990
else_env.creation_mark = leaf_mark
991
992
let target = _resolve_target(`isa.right)
993
let target_path: ACCESS_PATH? mut =
994
if !target? then _resolve_path(`isa.right) else null fi
995
let isa_type = `isa.type_expression.type
996
997
if target? \/ target_path? then
998
// `isa T(x)` is true only for a non-null `x` of a
999
// matching type — so the true edge knows both. Same
1000
// reasoning holds for a member-access path receiver.
1001
if target? then
1002
then_env.set_non_null(target)
1003
else
1004
then_env.set_non_null_path(target_path!)
1005
fi
1006
1007
_decline_unbacked_narrowing(
1008
`isa.right.location,
1009
if target? then target else null fi,
1010
if target? then null else target_path fi,
1011
true,
1012
true)
1013
1014
// When `isa V(x)` names a variant of `x`'s union, the
1015
// narrow target is the variant specialized with the
1016
// receiver's generic args (a bare `isa CONS(l)` for
1017
// `l: List[int]` narrows to `CONS[int]`, not the
1018
// unspecialized variant), and the else edge narrows
1019
// to the complement. Otherwise fall back to the
1020
// literal isa_type for the then edge with no else
1021
// narrow, since the complement of an open class
1022
// hierarchy (e.g. `Animal \ Cat`) isn't representable.
1023
let receiver_type = `isa.right?.value?.type
1024
1025
let isa_classy = get_classy_for_narrowing(isa_type)
1026
1027
let variant_narrow: Type? mut = null
1028
1029
if isa_classy? /\ isa_classy.is_variant /\ receiver_type? then
1030
// Only push a narrow when the variant is
1031
// actually a member of the receiver's narrowed
1032
// set — a statically-false `isa V(x)` shouldn't
1033
// mutate the then-environment. Specialization
1034
// for IL emission is handled separately at
1035
// compile_expressions.visit(ISA).
1036
let one_of: Semantic.Types.ONE_OF? mut = null
1037
if isa Semantic.Types.ONE_OF(receiver_type) then
1038
one_of = receiver_type
1039
fi
1040
1041
if !one_of? \/ one_of.contains_subtype(isa_classy) then
1042
variant_narrow = try_get_variant_type_for_classy(receiver_type, isa_classy)
1043
fi
1044
fi
1045
1046
let closed_subclass_narrow: Type? mut = null
1047
1048
if !variant_narrow? /\ isa_classy? /\ isa_classy.is_class /\ receiver_type? then
1049
closed_subclass_narrow =
1050
try_get_closed_subclass_narrow_type(receiver_type, isa_classy)
1051
fi
1052
1053
if let then_narrow =
1054
if variant_narrow? then variant_narrow else closed_subclass_narrow fi
1055
then
1056
// isa_classy and receiver_type are non-null whenever
1057
// then_narrow is — both narrow branches above
1058
// require them.
1059
let else_narrow: Type? mut = null
1060
1061
if target? then
1062
else_narrow = _set_narrow_with_complement(
1063
target, receiver_type!, then_narrow, isa_classy!, then_env, else_env)
1064
else
1065
else_narrow = _set_path_narrow_with_complement(
1066
target_path!, receiver_type!, then_narrow, isa_classy!, then_env, else_env)
1067
fi
1068
1069
_inlay(
1070
`isa.right.location,
1071
"narrowing-isa",
1072
"►",
1073
INLAY_TYPE.render(then_narrow)
1074
)
1075
1076
if else_narrow? then
1077
_decline_unbacked_narrowing(
1078
`isa.right.location,
1079
if target? then target else null fi,
1080
if target? then null else target_path fi,
1081
false,
1082
true)
1083
1084
_inlay(
1085
`isa.right.location,
1086
"narrowing-isa-complement",
1087
"►",
1088
INLAY_TYPE.render(else_narrow)
1089
)
1090
fi
1091
elif isa_type? /\ !isa_type.is_error /\ !isa_type.is_inferred then
1092
if target? then
1093
then_env.set_narrow(target, isa_type)
1094
else
1095
then_env.set_path_narrow(target_path!, isa_type)
1096
fi
1097
1098
_inlay(
1099
`isa.right.location,
1100
"narrowing-isa",
1101
"►",
1102
INLAY_TYPE.render(isa_type)
1103
)
1104
fi
1105
fi
1106
1107
return CONDITION_FACTS(then_env, else_env)
1108
si
1109
1110
// Narrow `target` to `then_narrow` on the true edge, and — when
1111
// representable — to the complement of `eliminated` within the
1112
// receiver's closed set on the false edge. Shared by `isa` and
1113
// the default-variant `?` present-test. Returns the complement
1114
// when one was applied, so the caller can announce the false
1115
// edge to the editor.
1116
_set_narrow_with_complement(
1117
target: Semantic.Symbols.Symbol,
1118
receiver_type: Type,
1119
then_narrow: Type,
1120
eliminated: Semantic.Symbols.Classy,
1121
then_env: NARROW_ENV,
1122
else_env: NARROW_ENV
1123
) -> Type? is
1124
then_env.set_narrow(target, then_narrow)
1125
1126
if let complement =
1127
_else_edge_complement(receiver_type, eliminated, else_env.is_non_null(target))
1128
then
1129
else_env.set_narrow(target, complement)
1130
return complement
1131
fi
1132
1133
return null
1134
si
1135
1136
// Path-keyed mirror of `_set_narrow_with_complement`. Same
1137
// then/else shape, keyed on ACCESS_PATH.
1138
_set_path_narrow_with_complement(
1139
target_path: ACCESS_PATH,
1140
receiver_type: Type,
1141
then_narrow: Type,
1142
eliminated: Semantic.Symbols.Classy,
1143
then_env: NARROW_ENV,
1144
else_env: NARROW_ENV
1145
) -> Type? is
1146
then_env.set_path_narrow(target_path, then_narrow)
1147
1148
if let complement =
1149
_else_edge_complement(receiver_type, eliminated, else_env.is_non_null_path(target_path))
1150
then
1151
else_env.set_path_narrow(target_path, complement)
1152
return complement
1153
fi
1154
1155
return null
1156
si
1157
1158
// The complement narrow for the false edge of an `isa` /
1159
// default-variant `?` test, with the null-free strip already
1160
// applied when the target was known present on the way in.
1161
// Null when the complement isn't representable.
1162
_else_edge_complement(
1163
receiver_type: Type,
1164
eliminated: Semantic.Symbols.Classy,
1165
was_non_null: bool
1166
) -> Type? is
1167
let eliminated_set = Collections.LIST[Semantic.Symbols.Classy]()
1168
eliminated_set.add(eliminated)
1169
1170
if let complement = try_get_complement_after_eliminated(receiver_type, eliminated_set) then
1171
if complement.is_optional /\ was_non_null then
1172
return complement.as_non_optional()
1173
fi
1174
1175
return complement
1176
fi
1177
1178
return null
1179
si
1180
1181
// `x?` on a union with a default variant compiles to
1182
// `isa Default(x)` (see compile_access.visit_has_value), so it
1183
// narrows the same way: the true edge to the default variant
1184
// specialized to the receiver, the false edge to the complement.
1185
// No-op when the default variant isn't in the receiver's
1186
// narrowed set (a statically-false `x?`).
1187
_narrow_default_variant_has_value(
1188
has_value_location: LOCATION,
1189
target: Semantic.Symbols.Symbol,
1190
receiver_type: Type,
1191
variant: Semantic.Symbols.Classy,
1192
then_env: NARROW_ENV,
1193
else_env: NARROW_ENV
1194
) is
1195
let one_of: Semantic.Types.ONE_OF? mut = null
1196
if isa Semantic.Types.ONE_OF(receiver_type) then
1197
one_of = receiver_type
1198
fi
1199
1200
if one_of? /\ !one_of.contains_subtype(variant) then
1201
return
1202
fi
1203
1204
if let variant_narrow = try_get_variant_type_for_classy(receiver_type, variant) then
1205
let else_narrow = _set_narrow_with_complement(
1206
target, receiver_type, variant_narrow, variant, then_env, else_env)
1207
1208
_decline_unbacked_narrowing(has_value_location, target, null, false, true)
1209
1210
_inlay(
1211
has_value_location,
1212
"narrowing-default-variant",
1213
"►",
1214
INLAY_TYPE.render(variant_narrow)
1215
)
1216
1217
if else_narrow? then
1218
_decline_unbacked_narrowing(
1219
has_value_location, target, null, false, true)
1220
1221
_inlay(
1222
has_value_location,
1223
"narrowing-default-variant-complement",
1224
"►",
1225
INLAY_TYPE.render(else_narrow)
1226
)
1227
fi
1228
fi
1229
si
1230
1231
// Path-keyed mirror of `_narrow_default_variant_has_value`.
1232
_narrow_default_variant_has_value_path(
1233
has_value_location: LOCATION,
1234
target_path: ACCESS_PATH,
1235
receiver_type: Type,
1236
variant: Semantic.Symbols.Classy,
1237
then_env: NARROW_ENV,
1238
else_env: NARROW_ENV
1239
) is
1240
let one_of: Semantic.Types.ONE_OF? mut = null
1241
if isa Semantic.Types.ONE_OF(receiver_type) then
1242
one_of = receiver_type
1243
fi
1244
1245
if one_of? /\ !one_of.contains_subtype(variant) then
1246
return
1247
fi
1248
1249
if let variant_narrow = try_get_variant_type_for_classy(receiver_type, variant) then
1250
let else_narrow = _set_path_narrow_with_complement(
1251
target_path, receiver_type, variant_narrow, variant, then_env, else_env)
1252
1253
_decline_unbacked_narrowing(has_value_location, null, target_path, false, true)
1254
1255
_inlay(
1256
has_value_location,
1257
"narrowing-default-variant",
1258
"►",
1259
INLAY_TYPE.render(variant_narrow)
1260
)
1261
1262
if else_narrow? then
1263
_decline_unbacked_narrowing(
1264
has_value_location, null, target_path, false, true)
1265
1266
_inlay(
1267
has_value_location,
1268
"narrowing-default-variant-complement",
1269
"►",
1270
INLAY_TYPE.render(else_narrow)
1271
)
1272
fi
1273
fi
1274
si
1275
1276
// `x?` — the has-value test. On the true edge `x` is known to
1277
// hold a value; for a union with a default variant both edges
1278
// narrow (see `_narrow_default_variant_has_value`). For an
1279
// optional reference or a NULLABLE[T] / MAYBE[T] value type the
1280
// false edge learns nothing (absence isn't representable).
1281
_analyze_has_value(has_value: Trees.Expressions.HAS_VALUE, in_env: NARROW_ENV) -> CONDITION_FACTS is
1282
let then_env = in_env.copy()
1283
let else_env = in_env.copy()
1284
1285
// Facts this leaf adds were established when its test
1286
// ran; calls the condition walked before it cannot have
1287
// invalidated them.
1288
let leaf_mark = _leaf_mark(has_value)
1289
then_env.creation_mark = leaf_mark
1290
else_env.creation_mark = leaf_mark
1291
1292
let target = _resolve_target(has_value.left)
1293
let target_path: ACCESS_PATH? mut =
1294
if !target? then _resolve_path(has_value.left) else null fi
1295
1296
if target? then
1297
let emit_presence_hint = _presence_hint_would_add(target)
1298
1299
then_env.set_non_null(target)
1300
1301
_decline_unbacked_narrowing(has_value.left.location, target, null, true, false)
1302
1303
if emit_presence_hint then
1304
_inlay(
1305
has_value.left.location,
1306
"narrowing-presence",
1307
"►",
1308
INLAY_TYPE.render(target.type!.as_non_optional())
1309
)
1310
fi
1311
1312
let receiver_type = has_value.left.value?.type
1313
1314
if receiver_type? then
1315
let union_classy =
1316
cast Semantic.Symbols.UNION?(
1317
get_classy_for_narrowing(
1318
pick_underlying_type(receiver_type)))
1319
1320
if union_classy? then
1321
if let default_variant = union_classy.default_variant then
1322
_narrow_default_variant_has_value(
1323
has_value.left.location,
1324
target, receiver_type, default_variant, then_env, else_env)
1325
fi
1326
fi
1327
fi
1328
elif target_path? then
1329
then_env.set_non_null_path(target_path)
1330
1331
_decline_unbacked_narrowing(has_value.left.location, null, target_path, true, false)
1332
1333
let path_type = has_value.left.value?.type
1334
1335
_inlay(
1336
has_value.left.location,
1337
"narrowing-presence",
1338
"►",
1339
if path_type? then INLAY_TYPE.render(path_type.as_non_optional()) else "" fi
1340
)
1341
1342
let receiver_type = has_value.left.value?.type
1343
1344
if receiver_type? then
1345
let union_classy =
1346
cast Semantic.Symbols.UNION?(
1347
get_classy_for_narrowing(
1348
pick_underlying_type(receiver_type)))
1349
1350
if union_classy? then
1351
if let default_variant = union_classy.default_variant then
1352
_narrow_default_variant_has_value_path(
1353
has_value.left.location,
1354
target_path, receiver_type, default_variant, then_env, else_env)
1355
fi
1356
fi
1357
fi
1358
fi
1359
1360
return CONDITION_FACTS(then_env, else_env)
1361
si
1362
1363
si
1364
si