Skip to content
← Back

src/syntax/process/compile-expressions/compile_expressions_narrowing.ghul

1
namespace Syntax.Process is
2
use System.Exception
3
4
use IO.Std
5
6
use Logging
7
use Source
8
9
use IR.Values
10
use IR.VALUE_CONVERTER
11
use IR.VALUE_BOXER
12
13
use Semantic.LEAST_UPPER_BOUND_MAP
14
use Semantic.Types.Type
15
16
use Syntax.Trees.Definitions.PRAGMA
17
18
use Ghul.Pipes
19
20
21
// Receiver-presence checks, narrowing targets, access paths, pure-slot checks and call bookkeeping.
22
partial COMPILE_EXPRESSIONS is
23
// If `expr` is an unqualified identifier resolving (in the
24
// current scope) to a narrowing subject, return that symbol;
25
// null otherwise. Subjects are local variables, fields, and
26
// properties whose getter is proven store-free — for those,
27
// re-reading under an unchanged heap repeats the same
28
// presence and dynamic-type answers, and the flow transfers
29
// (on_call / on_heap_store) forget them the moment the heap
30
// may have changed. A property with an unproven getter never
31
// narrows. Qualified and member-access targets are deferred.
32
// Reject a dereference through an optional receiver not
33
// proven to hold a value here. Optionality is one concept
34
// regardless of representation - a reference `T?`, a value-type
35
// NULLABLE[T], or an unconstrained MAYBE[T] all answer is_optional
36
// and are treated alike. Member access, indexing and `for ... in`
37
// all reach the receiver's members off its non-optional shape, so
38
// an un-narrowed optional receiver may be absent at the
39
// dereference. A receiver proven present by flow narrowing is
40
// safe. The same standing as "`'T?`' is not assignable to `'T?`'":
41
// the same fact about the same static type, so no slug and
42
// no suppression - narrow first (`!`, `if let`, `?.`) or copy
43
// the value into a local.
44
check_receiver_present(receiver: Trees.Expressions.Expression) is
45
if !receiver_may_be_absent(receiver) then
46
return
47
fi
48
49
let target = try_get_narrowing_target(receiver)
50
let subject = if target? then target.name else "receiver" fi
51
52
// Where the value was proven present earlier and a call
53
// ended that, the message is the ordinary one with the call
54
// it ended at.
55
let dropped = DROPPED_NARROWING.for_presence(target, try_build_access_path(receiver), receiver.location)
56
57
if let kill = dropped then
58
_logger.error(
59
receiver.location,
60
"{subject} may not hold a value here",
61
kill.crossing.location,
62
DROPPED_NARROWING.message(kill)
63
)
64
65
return
66
fi
67
68
_logger.error(receiver.location, "{subject} may not hold a value here")
69
si
70
71
// Whether a receiver could hold no value at this point: its type
72
// is optional and flow narrowing has not proven it present. The
73
// question behind the rejection above, asked separately because
74
// operator resolution asks it too - an operator whose left
75
// operand may be absent does not resolve at all, which is a
76
// typing rule rather than advice.
77
receiver_may_be_absent(receiver: Trees.Expressions.Expression) -> bool is
78
let value = receiver.value
79
80
if !value? then
81
return false
82
fi
83
84
let type = value.type
85
86
if !type? \/ !type.is_optional then
87
return false
88
fi
89
90
let target = try_get_narrowing_target(receiver)
91
92
return !(target? /\ _flow.is_non_null(target))
93
si
94
95
// The member an assignment writes, for the definite-assignment
96
// domain alone, written either `self.x = ...` or bare `x = ...`.
97
//
98
// The narrowing target answers a different question and declines
99
// both of these. It declines `self.x = x` where a parameter
100
// shadows the member, because a presence fact proven of the
101
// parameter must not be borrowed by the member - but the write
102
// happens whatever is shadowing, and the constructor idiom
103
// `init(x: T) is self.x = x; si` depends on that. And it declines
104
// a property whose getter is not proven store-free, because a
105
// getter that stores could invalidate what was narrowed - but
106
// "was it written" is settled by the write itself, and the getter
107
// of an auto-property on an `open` class is exactly the case that
108
// cannot be proven.
109
try_get_member_assigned(expr: Trees.Expressions.Expression?) -> Semantic.Symbols.Symbol? is
110
if !expr? then
111
return null
112
fi
113
114
let context = current_instance_context
115
116
if !context? then
117
return null
118
fi
119
120
if let self_member: Trees.Expressions.MEMBER = expr /\ isa Trees.Expressions.SELF(self_member.left) then
121
if self_member.is_coalesce \/ self_member.identifier.is_qualified then
122
return null
123
fi
124
125
return context.find_direct(self_member.identifier.name)
126
fi
127
128
if let identifier: Trees.Expressions.IDENTIFIER = expr then
129
if identifier.identifier.is_qualified then
130
return null
131
fi
132
133
// Resolved through the enclosing scope rather than
134
// straight off the instance, so a local or parameter of
135
// the same name is seen to shadow the member - a bare
136
// `x = ...` then writes the local and no member is
137
// assigned. `try_find` rather than `find` because this is
138
// speculative: `find` reports an unresolved name, and the
139
// real resolution of this expression reports it already.
140
let symbol = try_find(identifier.identifier)
141
142
if symbol? /\ symbol == context.find_direct(identifier.identifier.name) then
143
return symbol
144
fi
145
fi
146
147
return null
148
si
149
150
try_get_narrowing_target(expr: Trees.Expressions.Expression?) -> Semantic.Symbols.Symbol? is
151
if !expr? then
152
return null
153
fi
154
155
// `self` narrows like a local, keyed on its instance-context
156
// symbol. It can't be reassigned and an object's concrete type
157
// is fixed for its lifetime, so a narrowing on `self` is never
158
// killed by a call - it is sounder to narrow than a local.
159
if isa Trees.Expressions.SELF(expr) then
160
return current_instance_context
161
fi
162
163
// `self.x` usually names the same location the bare `x`
164
// does, and where it does the two spellings share one fact
165
// rather than keying their own — a guard written one way is
166
// then seen by a use written the other.
167
//
168
// Only where it does. The bare form resolves by lexical
169
// scope lookup, so a local or parameter of the same name
170
// shadows the member and the two spellings name different
171
// locations — the `init(name: string?)` that assigns
172
// `self.name = name` is the everyday case. Sharing a fact
173
// across that pair would narrow the member on presence
174
// proved of the parameter. So the member is looked up on
175
// the enclosing instance and the two must agree by
176
// identity; where they don't, neither spelling may borrow
177
// the other's fact and `self.x` narrows on nothing.
178
if let self_member: Trees.Expressions.MEMBER = expr /\ isa Trees.Expressions.SELF(self_member.left) then
179
if self_member.is_coalesce \/ self_member.identifier.is_qualified then
180
return null
181
fi
182
183
let context = current_instance_context
184
185
if !context? then
186
return null
187
fi
188
189
let target = _narrowing_target_named(self_member.identifier)
190
191
if !target? \/ target != context.find_direct(self_member.identifier.name) then
192
return null
193
fi
194
195
return target
196
fi
197
198
if !isa Trees.Expressions.IDENTIFIER(expr) then
199
return null
200
fi
201
202
return _narrowing_target_named(expr.identifier)
203
si
204
205
// The symbol an unqualified name narrows on, or null when it
206
// names nothing the flow analysis tracks: a local or field, or
207
// a property whose getter is proven store-free.
208
_narrowing_target_named(identifier: Trees.Identifiers.Identifier) -> Semantic.Symbols.Symbol? is
209
if identifier.is_qualified then
210
return null
211
fi
212
213
let symbol = find(identifier)
214
215
if !symbol? then
216
return null
217
fi
218
219
if isa Semantic.Symbols.Variable(symbol) then
220
return symbol
221
fi
222
223
if isa Semantic.Symbols.Property(symbol) then
224
let property = cast Semantic.Symbols.Property(symbol)
225
226
// Any readable property is a narrowing target; whether
227
// its getter backs the fact is settled at formation once
228
// the effect relations are solved - see
229
// NARROW_ENV._declines_getter_fact.
230
if property.read_function? then
231
return property
232
fi
233
fi
234
235
return null
236
si
237
238
// Build the re-readable access path an expression names, or
239
// null when it isn't one: a local- or field-rooted chain of
240
// instance field / store-free-getter reads (`receiver.prop`,
241
// `receiver.a.b`). Calls, indexers, `?.`, qualified names,
242
// self / super roots, struct receivers and properties with
243
// unproven getters are all excluded — the result must be a
244
// location that re-reads to the same presence answer under an
245
// unchanged heap, and the flow transfers drop its facts the
246
// moment the heap may have changed. Each hop is re-resolved
247
// through `find` / `find_member` so the check site (`x.y?`)
248
// and every use site (`x.y`) agree on the same root Variable
249
// and member Symbols by identity, and so key the same
250
// presence fact.
251
try_build_access_path(expr: Trees.Expressions.Expression?) -> ACCESS_PATH? is
252
if !expr? \/ !isa Trees.Expressions.MEMBER(expr) then
253
return null
254
fi
255
256
let member = cast Trees.Expressions.MEMBER(expr)
257
258
if member.is_coalesce \/ member.identifier.is_qualified then
259
return null
260
fi
261
262
let root: Semantic.Symbols.Symbol? mut = null
263
let members = Collections.LIST[Semantic.Symbols.Symbol]()
264
265
if isa Trees.Expressions.IDENTIFIER(member.left) then
266
let id = cast Trees.Expressions.IDENTIFIER(member.left)
267
268
// The root is subject to the same rule as a hop: a
269
// local or field read directly, or a property whose
270
// getter is proven store-free. `try_get_narrowing_target`
271
// is that rule, so a path root is exactly a symbol the
272
// flow analysis already tracks on its own — which is
273
// what makes the fact recorded at `h.name?` the one
274
// read back at every later `h.name`.
275
let symbol = try_get_narrowing_target(id)
276
277
if !symbol? then
278
return null
279
fi
280
281
root = symbol
282
else
283
let left_path = try_build_access_path(member.left)
284
285
if !left_path? then
286
return null
287
fi
288
289
root = left_path.root
290
291
for m in left_path.members do
292
members.add(m)
293
od
294
fi
295
296
let left_value = member.left.value
297
298
if !left_value? \/ !left_value.type? then
299
return null
300
fi
301
302
let left_type = left_value.type
303
304
if !isa Semantic.Types.NAMED(left_type) \/ left_type.is_value_type then
305
return null
306
fi
307
308
let hop = left_type.find_member(member.identifier.name)
309
310
if !hop? \/ !hop.is_instance then
311
return null
312
fi
313
314
if !hop.is_field then
315
let property = cast Semantic.Symbols.Property?(hop)
316
317
// as in try_get_narrowing_target: any readable
318
// property is an eligible hop, judged where a
319
// consumption leans on the fact
320
if !property? \/ !property.read_function? then
321
return null
322
fi
323
fi
324
325
members.add(hop)
326
327
return ACCESS_PATH(root, members)
328
si
329
330
_check_pure_slots(
331
location: LOCATION,
332
value: IR.Values.Value?,
333
syntax_arguments: Trees.Expressions.LIST?
334
) is
335
_pure_slots.check_call(location, value, syntax_arguments)
336
si
337
338
// Record a compiled call or construction as a crossing on
339
// every live heap fact — its receiver and arguments are
340
// already read, and the callee may reassign a field through
341
// an aliased receiver, so a later use of a fact carried over
342
// this point is judged against it. `location` is the source
343
// site of the expression: a Call value carries no ambient
344
// location of its own, so the crossing must be anchored to
345
// the syntax node rather than to `value.location`.
346
_note_call(location: LOCATION, value: IR.Values.Value?) is
347
_note_self_call(value)
348
349
if !value? \/ !value.is_state_changing_call then
350
// not a call at all, or a call whose callee is
351
// structurally store-free — declared pure, a
352
// whitelisted import, a synthesized backing read, a
353
// pure function-typed invocation. Only the structural
354
// tiers gate here: the proven answer does not exist
355
// until after the solve, and a crossing skipped on a
356
// stale proof could never be judged.
357
return
358
fi
359
360
_flow.on_call_of(location, _called_function_of(value))
361
si
362
363
// The single bounded callee behind a state-changing value, or
364
// null for a closure invocation or anything else with no one
365
// callee, which no judgement can discharge.
366
_called_function_of(value: IR.Values.Value?) -> Semantic.Symbols.Function? is
367
if let call: IR.Values.Call.INSTANCE = value then
368
return call.function
369
elif let call: IR.Values.Call.STATIC = value then
370
return call.function
371
elif let call: IR.Values.Call.GLOBAL = value then
372
return call.function
373
elif let call: IR.Values.Call.STRUCT = value then
374
return call.function
375
elif let construction: IR.Values.NEW = value then
376
return construction.constructor
377
elif let view: IR.Values.NARROW_VIEW = value then
378
return _called_function_of(view.underlying)
379
fi
380
381
return null
382
si
383
384
// A call on `self` to a method of the enclosing type that cannot
385
// be overridden, recorded on the must-call domain so a
386
// constructor can be credited with what the callee assigns.
387
//
388
// Overridable is excluded rather than resolved: a subclass
389
// override may assign nothing, and crediting the statically
390
// resolved body would hide the null the check exists to find.
391
_note_self_call(value: IR.Values.Value?) is
392
if let call: IR.Values.Call.INSTANCE = value then
393
if !call.from.is_self then
394
return
395
fi
396
397
let callee = call.function
398
399
if callee.is_virtual /\ !_cannot_be_overridden(callee) then
400
return
401
fi
402
403
_flow.mark_called(callee)
404
fi
405
si
406
407
// True when no override of `callee` can exist. Two conditions,
408
// and both are needed: none in this build, which
409
// `has_no_overriders` answers, and none possible from another
410
// assembly, which only the owner's closure answers. A ghūl
411
// class is closed to other assemblies unless declared `open`,
412
// so an absence of overriders is the whole story there and
413
// nowhere else — a trait, a struct, an imported type and an
414
// `open` class can each be extended downstream by an override
415
// that assigns nothing.
416
_cannot_be_overridden(callee: Semantic.Symbols.Function) -> bool is
417
// A constructor is reached through the type it constructs, so
418
// there is no slot for a subclass to take over — the runtime
419
// rejects a virtual `.ctor`, and the emitter never marks one.
420
// The symbol reports `is_virtual` all the same, so a
421
// constructor chaining to another would otherwise be declined
422
// wherever the owner is `open`.
423
if callee.is_constructor then
424
return true
425
fi
426
427
if !callee.has_no_overriders then
428
return false
429
fi
430
431
let owner = cast Semantic.Symbols.Classy?(callee.owner)
432
433
// A closed trait's implementor set is enumerable in the same
434
// way a closed class's subclass set is, so this could take
435
// one too. It is left to classes deliberately: what the
436
// trait case buys has not been measured, and getting it
437
// wrong silently drops a store the caller depends on.
438
return owner? /\ owner.is_class /\ !owner.is_open
439
si
440
441
// True iff `type` is a non-optional reference type — the kind
442
// of slot the non-optional-by-default check guards. Value
443
// types, `T?`, type variables, void and error/placeholder
444
// types are all excluded.
445
_is_non_optional_reference(type: Semantic.Types.Type?) -> bool =>
446
type? /\ type.is_named /\
447
!type.is_value_type /\ !type.is_optional /\
448
!type.is_type_variable /\ !type.is_void /\
449
!type.is_error /\ !type.is_inferred
450
451
// True iff a value of `source` static type is guaranteed
452
// castable to `target` at runtime. The plain assignability
453
// check covers the direct subtype case. For an INTERSECTION
454
// source (produced by class+trait flow narrowing — the
455
// `if isa T(x) then cast T(x)` idiom when T is a trait
456
// cross-cutting x's declared type), the intersection value
457
// IS every member, so a target assignable from any member
458
// is guaranteed to succeed.
459
_cast_target_covers_source(target: Semantic.Types.Type, source: Semantic.Types.Type) -> bool is
460
if target.is_assignable_from(source) then
461
return true
462
fi
463
464
if isa Semantic.Types.INTERSECTION(source) then
465
let intersection = source
466
467
for m in intersection.members do
468
if target.is_assignable_from(m) then
469
return true
470
fi
471
od
472
fi
473
474
return false
475
si
476
477
// Non-optional-by-default: warn when a `T?` local the flow
478
// analysis has not proven present reaches a non-optional
479
// reference slot. `x?` / `isa` / `if let` clear it; an
480
// explicit `x!` is exempt — the user took responsibility. A
481
// non-variable `T?` source — a call result, a field — is not
482
// flow-tracked, so it stays silent rather than risk a false
483
// positive. The bare `null` literal is handled separately, in
484
// visit(NULL), via its constraint. On by default;
485
// `--no-warn-non-optional` opts out. A warning, not an error,
486
// during the migration; the end state is `T?` not
487
// assignment-compatible with `T`.
488
check_non_optional(target: Semantic.Types.Type?, source: Trees.Expressions.Expression?, location: Source.LOCATION) is
489
if _build_flags.no_warn_non_optional \/ !target? \/ !source? then
490
return
491
fi
492
493
if !_is_non_optional_reference(target) then
494
return
495
fi
496
497
let value = source.value
498
499
if !value? \/ !value.type? then
500
return
501
fi
502
503
if value.type.is_optional /\ !isa Trees.Expressions.UNWRAP(source) then
504
let v = try_get_narrowing_target(source)
505
506
if v? /\ !_flow.is_non_null(v) then
507
if let kill = DROPPED_NARROWING.for_presence(v, null, location) then
508
_logger.warn(
509
location,
510
"non-optional",
511
"{target} expected but {v.name} may not hold a value",
512
kill.crossing.location,
513
DROPPED_NARROWING.message(kill)
514
)
515
else
516
_logger.warn(location, "non-optional", "{target} expected but {v.name} may not hold a value")
517
fi
518
fi
519
fi
520
si
521
522
si
523
si