Skip to content
← Back

src/semantic/dotnet/nullability.ghul

1
namespace Semantic.DotNet is
2
use Collections
3
4
use ATTRIBUTE_DATA = System.Reflection.CustomAttributeData
5
use TYPED_ARGUMENT = System.Reflection.CustomAttributeTypedArgument
6
7
// Emit and read `System.Runtime.CompilerServices.NullableAttribute`,
8
// the CLR-standard carrier for reference-type `?` annotations. ghūl's
9
// `?` postfix on reference types is purely a source-side annotation —
10
// the underlying IL type for `Foo?` and `Foo` is identical — so to
11
// survive cross-assembly reflection the annotation rides on a custom
12
// attribute applied to the slot (parameter, return, field, property).
13
//
14
// Format: a pre-order walk of the type tree, one byte per Type slot,
15
// with 2 = `?`, 1 = non-`?`, 0 = no opinion (value types,
16
// System.Nullable[T], Ghul.MAYBE[T] — each carries its optionality
17
// through its CLR type, so no per-slot marker is needed). Generic
18
// arguments contribute bytes after their parent's byte. A `List[T?]`
19
// field with reference-`T?` produces `[1, 2]`: List non-`?`, T `?`.
20
//
21
// The slot's NullableAttribute is dropped entirely when no position
22
// would be 2 — every position then defaults to non-`?` on the read
23
// side. Single-position slots use the compact `.ctor(uint8)` form;
24
// multi-position slots use `.ctor(uint8[])`.
25
class NULLABILITY is
26
// True iff `type` has at least one reference-`?` position
27
// anywhere in its tree — the gate for emitting the attribute
28
// on a slot. Value-type optionals (`int?` -> System.Nullable),
29
// `Ghul.MAYBE[T]`, and bare type variables already round-trip
30
// via their distinct CLR types; their nested arguments are
31
// still walked.
32
needs_attribute(type: Types.Type?) -> bool static is
33
let bytes = compute_bytes(type)
34
35
return needs_attribute_for_bytes(bytes)
36
si
37
38
// The per-position bytes for `type`. One per Type slot, pre-
39
// order: head byte for the type itself, then recurse into each
40
// entry of `arguments`. Empty `[0]` for null / sentinel /
41
// error — those carry no nullability info either way.
42
compute_bytes(type: Types.Type?) -> LIST[int] static is
43
let result = LIST[int]()
44
45
_append_bytes(type, result)
46
47
return result
48
si
49
50
_append_bytes(type: Types.Type?, buffer: LIST[int]) static is
51
if !type? \/ type.is_sentinel \/ type.is_error then
52
buffer.add(0)
53
return
54
fi
55
56
buffer.add(_head_byte(type))
57
58
let arguments = type.arguments
59
60
for i in 0..arguments.count do
61
_append_bytes(arguments[i], buffer)
62
od
63
si
64
65
// The single byte for `type` at its own position. Value types
66
// (including System.Nullable[T] and Ghul.MAYBE[T]) return 0:
67
// their optionality rides on the CLR type itself, not on this
68
// attribute. Reference types and bare type variables return
69
// 1 / 2 from `is_optional`.
70
_head_byte(type: Types.Type?) -> int static is
71
if !type? then
72
return 0
73
fi
74
75
if type.is_value_type \/ type.is_maybe then
76
return 0
77
fi
78
79
if type.is_optional then
80
return 2
81
fi
82
83
return 1
84
si
85
86
needs_attribute_for_bytes(bytes: List[int]) -> bool static is
87
if bytes.count == 0 then
88
return false
89
fi
90
91
for b in bytes do
92
if b == 2 then
93
return true
94
fi
95
od
96
97
return false
98
si
99
100
// The position bytes carried by a `NullableAttribute` in
101
// `attributes`, expanded out for the read side to walk against
102
// the reflected type. Returns null when no attribute is
103
// present, or empty when the attribute is the byte[] form
104
// with a zero-length array. The single-byte form is returned
105
// as a one-element list — the caller broadcasts it across
106
// every position via `apply_bytes`.
107
read_bytes(attributes: Iterable[ATTRIBUTE_DATA]?) -> LIST[int]? static is
108
if !attributes? then
109
return null
110
fi
111
112
try
113
for attribute in attributes do
114
let attribute_type = attribute.attribute_type
115
116
if attribute_type.full_name =~ "System.Runtime.CompilerServices.NullableAttribute" then
117
for constructor_argument in attribute.constructor_arguments do
118
return _read_bytes_from_argument(constructor_argument)
119
od
120
fi
121
od
122
catch ex: System.Exception
123
yrt
124
125
return null
126
si
127
128
// The default-byte from `NullableContextAttribute(byte)` in
129
// `attributes`, or null when no such attribute is present.
130
// C# emits this attribute at member or type scope to set the
131
// default per-position nullability byte for any slot that
132
// has no per-slot `NullableAttribute`. The BCL relies on
133
// it heavily: a class-scoped `NullableContextAttribute(1)`
134
// covers every reference position that would otherwise
135
// read as oblivious.
136
read_context_byte(attributes: Iterable[ATTRIBUTE_DATA]?) -> int? static is
137
if !attributes? then
138
return null
139
fi
140
141
try
142
for attribute in attributes do
143
let attribute_type = attribute.attribute_type
144
145
if attribute_type.full_name =~ "System.Runtime.CompilerServices.NullableContextAttribute" then
146
for constructor_argument in attribute.constructor_arguments do
147
let arg_type = constructor_argument.argument_type
148
if arg_type.full_name =~ "System.Byte" then
149
return System.Convert.to_int32(constructor_argument.value)
150
fi
151
od
152
fi
153
od
154
catch ex: System.Exception
155
yrt
156
157
return null
158
si
159
160
// Resolve `type`'s reference-`?` annotations from the slot's
161
// per-slot `NullableAttribute` if present, otherwise from the
162
// enclosing `NullableContextAttribute` chain walked from
163
// `member` outward. `clr_type` is the System.Type the slot
164
// declares (parameter / return / field / property type); it
165
// drives the tree-aware byte-list construction for the
166
// context fallback so value-type and MAYBE[T] positions get
167
// 0 regardless of the context default. CLR queries
168
// (`is_value_type`, `get_generic_arguments`) don't force
169
// ghūl symbol materialization — that's the safe substitute
170
// for asking the ghūl Type the same question at apply time.
171
resolve(
172
type: Types.Type?,
173
slot_attributes: Iterable[ATTRIBUTE_DATA],
174
member: System.Reflection.MemberInfo,
175
clr_type: System.Type
176
) -> Types.Type? static is
177
if !type? then
178
return type
179
fi
180
181
let bytes = read_bytes(slot_attributes)
182
183
if bytes? /\ bytes.count > 0 then
184
// The compact single-byte form broadcasts over the
185
// flag-capable positions only — a value-type position
186
// must stay 0 even under a broadcast 2, or char[]?
187
// reads back as char?[]. Expand it through the same
188
// CLR-walking helper the context fallback uses; the
189
// CLR queries don't force symbol materialization.
190
if bytes.count == 1 then
191
return apply_bytes(type, compute_broadcast_bytes(clr_type, bytes[0]))
192
fi
193
194
return apply_bytes(type, bytes)
195
fi
196
197
let context = read_context_chain(member)
198
199
if !context? \/ context == 0 then
200
return type
201
fi
202
203
return apply_bytes(type, compute_broadcast_bytes(clr_type, context))
204
si
205
206
// The effective `NullableContextAttribute` byte for `member`:
207
// first the member's own attributes, then its `declaring_type`
208
// chain walked outward. Returns null when no
209
// `NullableContextAttribute` is found anywhere in the chain.
210
// No cache — the chain depth for any one reflected member is
211
// typically 1; revisit if profiling says otherwise.
212
read_context_chain(member: System.Reflection.MemberInfo?) -> int? static is
213
if !member? then
214
return null
215
fi
216
217
try
218
let own = read_context_byte(member.get_custom_attributes_data())
219
220
if own? then
221
return own
222
fi
223
224
let t: System.Type? mut = member.declaring_type
225
226
while t? do
227
let from_t = read_context_byte(t.get_custom_attributes_data())
228
229
if from_t? then
230
return from_t
231
fi
232
233
t = t.declaring_type
234
od
235
catch ex: System.Exception
236
yrt
237
238
return null
239
si
240
241
// Per-position byte list for a context-default broadcast over
242
// `clr_type`'s tree. Value-type and `Ghul.MAYBE[T]` positions
243
// get 0 (their optionality rides on the CLR type itself);
244
// every other position gets `default_byte`. Walks the CLR
245
// Type, not the ghūl Type — System.Type queries don't force
246
// a lazy `TYPE_WRAPPER` symbol to materialize, which the
247
// ghūl-side `is_value_type` / `is_maybe` queries would.
248
compute_broadcast_bytes(clr_type: System.Type, default_byte: int) -> LIST[int] static is
249
let result = LIST[int]()
250
251
_append_broadcast(clr_type, default_byte, result)
252
253
return result
254
si
255
256
_append_broadcast(clr_type: System.Type?, default_byte: int, buffer: LIST[int]) static is
257
if !clr_type? then
258
buffer.add(0)
259
return
260
fi
261
262
// Value-type positions never carry the reference-`?`
263
// marker — `Nullable[T]` and `Ghul.MAYBE[T]` (both
264
// structs at the CLR level) carry optionality through
265
// their CLR type. The byte at this position is 0
266
// regardless of the broadcast default.
267
if clr_type.is_value_type then
268
buffer.add(0)
269
else
270
buffer.add(default_byte)
271
fi
272
273
if clr_type.is_generic_type /\ !clr_type.is_generic_type_definition then
274
for arg in clr_type.get_generic_arguments() do
275
_append_broadcast(arg, default_byte, buffer)
276
od
277
elif clr_type.is_array \/ clr_type.is_by_ref \/ clr_type.is_pointer then
278
_append_broadcast(clr_type.get_element_type(), default_byte, buffer)
279
fi
280
si
281
282
_read_bytes_from_argument(argument: TYPED_ARGUMENT) -> LIST[int]? static is
283
let arg_type = argument.argument_type
284
285
if arg_type.full_name =~ "System.Byte" then
286
let result = LIST[int]()
287
result.add(System.Convert.to_int32(argument.value))
288
return result
289
fi
290
291
// The byte[] form surfaces as an Iterable of one
292
// CustomAttributeTypedArgument per element; each element's
293
// .value is a boxed byte.
294
let elements = cast Iterable[TYPED_ARGUMENT]?(argument.value)
295
296
if !elements? then
297
return null
298
fi
299
300
let result = LIST[int]()
301
302
for element in elements do
303
let element_type = element.argument_type
304
305
if !(element_type.full_name =~ "System.Byte") then
306
return null
307
fi
308
309
result.add(System.Convert.to_int32(element.value))
310
od
311
312
return result
313
si
314
315
// Return `type` with each `?` position from `bytes` applied to
316
// the matching position in its tree. The walk mirrors the
317
// emitter: head byte at the current position, then recurse
318
// into `arguments` left-to-right. A single-byte attribute
319
// broadcasts to every position. Generics with at least one
320
// changed argument are rebuilt via `GENERIC.create`, which
321
// each subclass overrides to preserve its identity (ARRAY,
322
// NULLABLE, MAYBE, REFERENCE, POINTER, TUPLE). Symbol
323
// materialization is deferred until we actually have to
324
// rebuild — slots whose tree has no `?` position never force
325
// a TYPE_WRAPPER's symbol, preserving the bootstrap-safe path
326
// the existing reflection import depends on.
327
apply_bytes(type: Types.Type?, bytes: List[int]) -> Types.Type? static is
328
if !type? \/ bytes.count == 0 then
329
return type
330
fi
331
332
let (result, _, _) = _apply_walk(type, bytes, 0)
333
334
return result
335
si
336
337
// Returns (new_type, next_index, any_changed). `any_changed`
338
// propagates up so a parent generic only rebuilds when one of
339
// its arguments actually had a `?` applied; that keeps the
340
// bootstrap-safe path intact for slots whose tree carries
341
// only 1s or 0s.
342
_apply_walk(
343
type: Types.Type,
344
bytes: List[int],
345
start_index: int
346
) -> (Types.Type, int, bool) static is
347
let position = start_index
348
let index mut = start_index + 1
349
350
// a null type carries no nullability bytes to apply
351
@suppress("presence-test-non-optional")
352
if !type? then
353
return (type, index, false)
354
fi
355
356
let head = _byte_at(bytes, position)
357
358
let arguments = type.arguments
359
360
if arguments.count > 0 then
361
let new_arguments = LIST[Types.Type]()
362
let any_arg_changed mut = false
363
364
for i in 0..arguments.count do
365
let (new_arg, next_index, arg_changed)
366
= _apply_walk(arguments[i], bytes, index)
367
368
index = next_index
369
370
if arg_changed then
371
any_arg_changed = true
372
fi
373
374
new_arguments.add(new_arg)
375
od
376
377
let rebuilt mut = type
378
379
if any_arg_changed then
380
if let generic: Types.GENERIC = type then
381
let rebuilt_generic = rebuild_generic(generic, new_arguments)
382
383
if rebuilt_generic? then
384
rebuilt = rebuilt_generic
385
fi
386
fi
387
fi
388
389
if head == 2 then
390
return (rebuilt.as_optional_unchecked(), index, true)
391
fi
392
393
return (rebuilt, index, any_arg_changed)
394
fi
395
396
if head == 2 then
397
return (type.as_optional_unchecked(), index, true)
398
fi
399
400
return (type, index, false)
401
si
402
403
_byte_at(bytes: List[int], position: int) -> int static is
404
if bytes.count == 1 then
405
return bytes[0]
406
fi
407
408
if position < 0 \/ position >= bytes.count then
409
return 0
410
fi
411
412
return bytes[position]
413
si
414
415
// Construct a fresh GENERIC with `new_arguments` by dispatching
416
// through `GENERIC.create` — the same hook each subclass uses
417
// for specialization, so an ARRAY stays an ARRAY, a NULLABLE
418
// stays a NULLABLE, etc. Returns null when the underlying
419
// Classy can't be reached (e.g. the wrapper's symbol failed to
420
// materialize during early bootstrap); the caller falls back
421
// to the non-rebuilt path so reads stay best-effort instead
422
// of crashing.
423
rebuild_generic(
424
generic: Types.GENERIC,
425
new_arguments: List[Types.Type]
426
) -> Types.GENERIC? static is
427
try
428
let generic_symbol = cast Symbols.GENERIC?(generic.symbol)
429
430
if !generic_symbol? then
431
return null
432
fi
433
434
return generic.create(
435
Source.LOCATION.reflected,
436
generic_symbol.symbol,
437
new_arguments
438
)
439
catch ex: System.Exception
440
return null
441
yrt
442
si
443
si
444
si