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
// The full `.custom instance void ...NullableAttribute::.ctor(...) = (...)`
87
// directive line for `type`, or null when the slot needs no
88
// attribute. Picks `.ctor(uint8)` for the single-position case
89
// and `.ctor(uint8[])` for the rest.
90
gen_attribute_line_for_type(type: Types.Type) -> string? static is
91
let bytes = compute_bytes(type);
92
93
if !needs_attribute_for_bytes(bytes) then
94
return null;
95
fi
96
97
return gen_attribute_line_for_bytes(bytes);
98
si
99
100
needs_attribute_for_bytes(bytes: List[int]) -> bool static is
101
if bytes.count == 0 then
102
return false;
103
fi
104
105
for b in bytes do
106
if b == 2 then
107
return true;
108
fi
109
od
110
111
return false;
112
si
113
114
// A single position uses `.ctor(uint8)`, anything else the
115
// `.ctor(uint8[])` overload.
116
gen_attribute_arguments_for_bytes(bytes: List[int]) -> string static is
117
if bytes.count == 1 then
118
return "{{ {IL_ATTRIBUTE_ARGUMENTS.byte_argument(bytes[0])} }}";
119
fi
120
121
return "{{ {IL_ATTRIBUTE_ARGUMENTS.byte_array_argument(bytes)} }}";
122
si
123
124
gen_attribute_line_for_bytes(bytes: List[int]) -> string static is
125
let arguments = gen_attribute_arguments_for_bytes(bytes);
126
127
if bytes.count == 1 then
128
return ".custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = {arguments}";
129
fi
130
131
return ".custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = {arguments}";
132
si
133
134
// The position bytes carried by a `NullableAttribute` in
135
// `attributes`, expanded out for the read side to walk against
136
// the reflected type. Returns null when no attribute is
137
// present, or empty when the attribute is the byte[] form
138
// with a zero-length array. The single-byte form is returned
139
// as a one-element list — the caller broadcasts it across
140
// every position via `apply_bytes`.
141
read_bytes(attributes: Iterable[ATTRIBUTE_DATA]?) -> LIST[int]? static is
142
if !attributes? then
143
return null;
144
fi
145
146
try
147
for attribute in attributes do
148
let attribute_type = attribute.attribute_type;
149
150
if attribute_type.full_name =~ "System.Runtime.CompilerServices.NullableAttribute" then
151
for constructor_argument in attribute.constructor_arguments do
152
return _read_bytes_from_argument(constructor_argument);
153
od
154
fi
155
od
156
catch ex: System.Exception
157
yrt
158
159
return null;
160
si
161
162
// The default-byte from `NullableContextAttribute(byte)` in
163
// `attributes`, or null when no such attribute is present.
164
// C# emits this attribute at member or type scope to set the
165
// default per-position nullability byte for any slot that
166
// has no per-slot `NullableAttribute`. The BCL relies on
167
// it heavily: a class-scoped `NullableContextAttribute(1)`
168
// covers every reference position that would otherwise
169
// read as oblivious.
170
read_context_byte(attributes: Iterable[ATTRIBUTE_DATA]?) -> int? static is
171
if !attributes? then
172
return null;
173
fi
174
175
try
176
for attribute in attributes do
177
let attribute_type = attribute.attribute_type;
178
179
if attribute_type.full_name =~ "System.Runtime.CompilerServices.NullableContextAttribute" then
180
for constructor_argument in attribute.constructor_arguments do
181
let arg_type = constructor_argument.argument_type;
182
if arg_type.full_name =~ "System.Byte" then
183
return System.Convert.to_int32(constructor_argument.value);
184
fi
185
od
186
fi
187
od
188
catch ex: System.Exception
189
yrt
190
191
return null;
192
si
193
194
// Resolve `type`'s reference-`?` annotations from the slot's
195
// per-slot `NullableAttribute` if present, otherwise from the
196
// enclosing `NullableContextAttribute` chain walked from
197
// `member` outward. `clr_type` is the System.Type the slot
198
// declares (parameter / return / field / property type); it
199
// drives the tree-aware byte-list construction for the
200
// context fallback so value-type and MAYBE[T] positions get
201
// 0 regardless of the context default. CLR queries
202
// (`is_value_type`, `get_generic_arguments`) don't force
203
// ghūl symbol materialization — that's the safe substitute
204
// for asking the ghūl Type the same question at apply time.
205
resolve(
206
type: Types.Type?,
207
slot_attributes: Iterable[ATTRIBUTE_DATA],
208
member: System.Reflection.MemberInfo,
209
clr_type: System.Type
210
) -> Types.Type? static is
211
if !type? then
212
return type;
213
fi
214
215
let bytes = read_bytes(slot_attributes);
216
217
if bytes? /\ bytes.count > 0 then
218
// The compact single-byte form broadcasts over the
219
// flag-capable positions only — a value-type position
220
// must stay 0 even under a broadcast 2, or char[]?
221
// reads back as char?[]. Expand it through the same
222
// CLR-walking helper the context fallback uses; the
223
// CLR queries don't force symbol materialization.
224
if bytes.count == 1 then
225
return apply_bytes(type, compute_broadcast_bytes(clr_type, bytes[0]));
226
fi
227
228
return apply_bytes(type, bytes);
229
fi
230
231
let context = read_context_chain(member);
232
233
if !context? \/ context == 0 then
234
return type;
235
fi
236
237
return apply_bytes(type, compute_broadcast_bytes(clr_type, context));
238
si
239
240
// The effective `NullableContextAttribute` byte for `member`:
241
// first the member's own attributes, then its `declaring_type`
242
// chain walked outward. Returns null when no
243
// `NullableContextAttribute` is found anywhere in the chain.
244
// No cache — the chain depth for any one reflected member is
245
// typically 1; revisit if profiling says otherwise.
246
read_context_chain(member: System.Reflection.MemberInfo?) -> int? static is
247
if !member? then
248
return null;
249
fi
250
251
try
252
let own = read_context_byte(member.get_custom_attributes_data());
253
254
if own? then
255
return own;
256
fi
257
258
let t: System.Type? mut = member.declaring_type;
259
260
while t? do
261
let from_t = read_context_byte(t.get_custom_attributes_data());
262
263
if from_t? then
264
return from_t;
265
fi
266
267
t = t.declaring_type;
268
od
269
catch ex: System.Exception
270
yrt
271
272
return null;
273
si
274
275
// Per-position byte list for a context-default broadcast over
276
// `clr_type`'s tree. Value-type and `Ghul.MAYBE[T]` positions
277
// get 0 (their optionality rides on the CLR type itself);
278
// every other position gets `default_byte`. Walks the CLR
279
// Type, not the ghūl Type — System.Type queries don't force
280
// a lazy `TYPE_WRAPPER` symbol to materialize, which the
281
// ghūl-side `is_value_type` / `is_maybe` queries would.
282
compute_broadcast_bytes(clr_type: System.Type, default_byte: int) -> LIST[int] static is
283
let result = LIST[int]();
284
285
_append_broadcast(clr_type, default_byte, result);
286
287
return result;
288
si
289
290
_append_broadcast(clr_type: System.Type?, default_byte: int, buffer: LIST[int]) static is
291
if !clr_type? then
292
buffer.add(0);
293
return;
294
fi
295
296
// Value-type positions never carry the reference-`?`
297
// marker — `Nullable[T]` and `Ghul.MAYBE[T]` (both
298
// structs at the CLR level) carry optionality through
299
// their CLR type. The byte at this position is 0
300
// regardless of the broadcast default.
301
if clr_type.is_value_type then
302
buffer.add(0);
303
else
304
buffer.add(default_byte);
305
fi
306
307
if clr_type.is_generic_type /\ !clr_type.is_generic_type_definition then
308
for arg in clr_type.get_generic_arguments() do
309
_append_broadcast(arg, default_byte, buffer);
310
od
311
elif clr_type.is_array \/ clr_type.is_by_ref \/ clr_type.is_pointer then
312
_append_broadcast(clr_type.get_element_type(), default_byte, buffer);
313
fi
314
si
315
316
_read_bytes_from_argument(argument: TYPED_ARGUMENT) -> LIST[int]? static is
317
let arg_type = argument.argument_type;
318
319
if arg_type.full_name =~ "System.Byte" then
320
let result = LIST[int]();
321
result.add(System.Convert.to_int32(argument.value));
322
return result;
323
fi
324
325
// The byte[] form surfaces as an Iterable of one
326
// CustomAttributeTypedArgument per element; each element's
327
// .value is a boxed byte.
328
let elements = cast Iterable[TYPED_ARGUMENT]?(argument.value);
329
330
if !elements? then
331
return null;
332
fi
333
334
let result = LIST[int]();
335
336
for element in elements do
337
let element_type = element.argument_type;
338
339
if !(element_type.full_name =~ "System.Byte") then
340
return null;
341
fi
342
343
result.add(System.Convert.to_int32(element.value));
344
od
345
346
return result;
347
si
348
349
// Return `type` with each `?` position from `bytes` applied to
350
// the matching position in its tree. The walk mirrors the
351
// emitter: head byte at the current position, then recurse
352
// into `arguments` left-to-right. A single-byte attribute
353
// broadcasts to every position. Generics with at least one
354
// changed argument are rebuilt via `GENERIC.create`, which
355
// each subclass overrides to preserve its identity (ARRAY,
356
// NULLABLE, MAYBE, REFERENCE, POINTER, TUPLE). Symbol
357
// materialization is deferred until we actually have to
358
// rebuild — slots whose tree has no `?` position never force
359
// a TYPE_WRAPPER's symbol, preserving the bootstrap-safe path
360
// the existing reflection import depends on.
361
apply_bytes(type: Types.Type, bytes: List[int]) -> Types.Type static is
362
if bytes.count == 0 then
363
return type;
364
fi
365
366
let (result, _, _) = _apply_walk(type, bytes, 0);
367
368
return result;
369
si
370
371
// Returns (new_type, next_index, any_changed). `any_changed`
372
// propagates up so a parent generic only rebuilds when one of
373
// its arguments actually had a `?` applied; that keeps the
374
// bootstrap-safe path intact for slots whose tree carries
375
// only 1s or 0s.
376
_apply_walk(
377
type: Types.Type,
378
bytes: List[int],
379
start_index: int
380
) -> (Types.Type, int, bool) static is
381
let position = start_index;
382
let index mut = start_index + 1;
383
384
// a null type carries no nullability bytes to apply
385
@suppress("presence-test-non-optional")
386
if !type? then
387
return (type, index, false);
388
fi
389
390
let head = _byte_at(bytes, position);
391
392
let arguments = type.arguments;
393
394
if arguments.count > 0 then
395
let new_arguments = LIST[Types.Type]();
396
let any_arg_changed mut = false;
397
398
for i in 0..arguments.count do
399
let (new_arg, next_index, arg_changed)
400
= _apply_walk(arguments[i], bytes, index);
401
402
index = next_index;
403
404
if arg_changed then
405
any_arg_changed = true;
406
fi
407
408
new_arguments.add(new_arg);
409
od
410
411
let rebuilt mut = type;
412
413
if any_arg_changed then
414
if let generic: Types.GENERIC = type then
415
let rebuilt_generic = _rebuild_generic(generic, new_arguments);
416
417
if rebuilt_generic? then
418
rebuilt = rebuilt_generic;
419
fi
420
fi
421
fi
422
423
if head == 2 then
424
return (rebuilt.as_optional_unchecked(), index, true);
425
fi
426
427
return (rebuilt, index, any_arg_changed);
428
fi
429
430
if head == 2 then
431
return (type.as_optional_unchecked(), index, true);
432
fi
433
434
return (type, index, false);
435
si
436
437
_byte_at(bytes: List[int], position: int) -> int static is
438
if bytes.count == 1 then
439
return bytes[0];
440
fi
441
442
if position < 0 \/ position >= bytes.count then
443
return 0;
444
fi
445
446
return bytes[position];
447
si
448
449
// Construct a fresh GENERIC with `new_arguments` by dispatching
450
// through `GENERIC.create` — the same hook each subclass uses
451
// for specialization, so an ARRAY stays an ARRAY, a NULLABLE
452
// stays a NULLABLE, etc. Returns null when the underlying
453
// Classy can't be reached (e.g. the wrapper's symbol failed to
454
// materialize during early bootstrap); the caller falls back
455
// to the non-rebuilt path so reads stay best-effort instead
456
// of crashing.
457
_rebuild_generic(
458
generic: Types.GENERIC,
459
new_arguments: List[Types.Type]
460
) -> Types.GENERIC? static is
461
try
462
let generic_symbol = cast Symbols.GENERIC?(generic.symbol);
463
464
if !generic_symbol? then
465
return null;
466
fi
467
468
return generic.create(
469
Source.LOCATION.reflected,
470
generic_symbol.symbol,
471
new_arguments
472
);
473
catch ex: System.Exception
474
return null;
475
yrt
476
si
477
si
478
si