Skip to content
← Back

src/syntax/process/attribute_resolver.ghul

1
namespace Syntax.Process is
2
use Logging;
3
use Source;
4
5
use Semantic.Types.Type;
6
7
use System.Text.StringBuilder;
8
9
// Resolves an attribute pragma — a pragma whose name is not a built-in
10
// compiler pragma — to a .NET attribute type and constructor, formats
11
// its arguments into the ilasm textual custom-attribute form, and
12
// attaches the resolved CUSTOM_ATTRIBUTE to the target symbol so the
13
// IL generator can emit a `.custom` directive.
14
class ATTRIBUTE_RESOLVER is
15
_logger: Logger;
16
_innate_symbol_lookup: Semantic.Lookups.InnateSymbolLookup;
17
_overload_resolver: Semantic.OVERLOAD_RESOLVER;
18
_visitor: ScopedVisitor;
19
20
init(
21
logger: Logger,
22
innate_symbol_lookup: Semantic.Lookups.InnateSymbolLookup,
23
overload_resolver: Semantic.OVERLOAD_RESOLVER,
24
visitor: ScopedVisitor
25
) is
26
super.init();
27
28
_logger = logger;
29
_innate_symbol_lookup = innate_symbol_lookup;
30
_overload_resolver = overload_resolver;
31
_visitor = visitor;
32
si
33
34
// A built-in pragma is handled by the compiler itself; anything
35
// else is taken to name a .NET attribute.
36
is_built_in(name: string) -> bool =>
37
name.starts_with("IL.") \/
38
name.starts_with("IF.") \/
39
name =~ "precedence" \/
40
name =~ "test" \/
41
name =~ "test_class" \/
42
name =~ "test_method" \/
43
name =~ "entry" \/
44
name =~ "suppress";
45
46
resolve(pragma: Trees.Pragmas.PRAGMA, target: Semantic.Symbols.Symbol?) is
47
if !target? then
48
return;
49
fi
50
51
let name = pragma.name.to_string();
52
53
if is_built_in(name) then
54
return;
55
fi
56
57
let attribute_type = resolve_attribute_type(pragma.name);
58
59
if !attribute_type? then
60
_logger.error(pragma.name.location, "unknown attribute {name}");
61
return;
62
fi
63
64
let positional = gather_positional_arguments(pragma);
65
66
let constructor = resolve_constructor(pragma, attribute_type, positional);
67
68
if !constructor? then
69
return;
70
fi
71
72
let arguments_text = format_arguments(pragma, attribute_type, constructor, positional);
73
74
if !arguments_text? then
75
return;
76
fi
77
78
if !target.custom_attributes? then
79
target.custom_attributes = Collections.LIST[Semantic.CUSTOM_ATTRIBUTE]();
80
fi
81
82
target.custom_attributes!.add(Semantic.CUSTOM_ATTRIBUTE(constructor, arguments_text));
83
si
84
85
// Resolve the pragma name to a .NET attribute class, accepting the
86
// `Foo` short form for a `FooAttribute` — whether `Foo` is written
87
// bare or fully qualified (`A.B.Foo` resolves `A.B.FooAttribute`).
88
resolve_attribute_type(name: Trees.Identifiers.Identifier) -> Semantic.Symbols.Classy? is
89
let direct = as_classy(_visitor.try_find(name));
90
91
if direct? then
92
return direct;
93
fi
94
95
let suffixed_name = "{name.name}Attribute";
96
97
if name.is_qualified then
98
let qualifier = _visitor.try_find(name.qualifier!);
99
100
if !qualifier? then
101
return null;
102
fi
103
104
return as_classy(qualifier.find_member(suffixed_name));
105
fi
106
107
return as_classy(_visitor.find(suffixed_name));
108
si
109
110
as_classy(symbol: Semantic.Symbols.Symbol?) -> Semantic.Symbols.Classy? is
111
if !symbol? then
112
return null;
113
fi
114
115
// A name shared across generic arities resolves to a TYPE_GROUP.
116
// Applying an attribute supplies no type arguments, so the
117
// non-generic member is the intended one.
118
if let group: Semantic.Symbols.TYPE_GROUP = symbol then
119
return group.find_by_generic_arguments_count(0);
120
fi
121
122
return cast Semantic.Symbols.Classy?(symbol);
123
si
124
125
gather_positional_arguments(pragma: Trees.Pragmas.PRAGMA) -> Collections.LIST[IR.Values.Value] is
126
let result = Collections.LIST[IR.Values.Value]();
127
128
for argument in pragma.arguments.expressions do
129
if argument.value? then
130
result.add(argument.value);
131
fi
132
od
133
134
return result;
135
si
136
137
resolve_constructor(
138
pragma: Trees.Pragmas.PRAGMA,
139
attribute_type: Semantic.Symbols.Classy,
140
positional: Collections.List[IR.Values.Value]
141
) -> Semantic.Symbols.Function? is
142
let init_group = cast Semantic.Symbols.FUNCTION_GROUP?(attribute_type.find_member("init"));
143
144
if !init_group? then
145
_logger.error(pragma.location, "attribute {attribute_type.name} has no usable constructor");
146
return null;
147
fi
148
149
let argument_types = Collections.LIST[Type]();
150
151
for argument in positional do
152
if argument.type? then
153
argument_types.add(argument.type!);
154
else
155
argument_types.add(Semantic.Types.ERROR());
156
fi
157
od
158
159
_logger.speculate();
160
161
let overload_result = _overload_resolver.resolve(pragma.location, init_group, argument_types, false, true, true);
162
163
if overload_result? then
164
_logger.commit();
165
return overload_result.function;
166
fi
167
168
let with_empty_arrays = resolve_constructor_with_empty_arrays(init_group, positional, argument_types);
169
170
if with_empty_arrays? then
171
_logger.roll_back();
172
return with_empty_arrays;
173
fi
174
175
let with_omitted_optionals = resolve_constructor_with_omitted_optionals(init_group, argument_types);
176
177
if with_omitted_optionals? then
178
_logger.roll_back();
179
return with_omitted_optionals;
180
fi
181
182
_logger.commit();
183
_logger.error(pragma.location, "no constructor of attribute {attribute_type.name} matches the supplied arguments");
184
return null;
185
si
186
187
// An empty array literal argument carries no element type of its
188
// own, so it falls back to object[] and fails to match a more
189
// specific array parameter. An empty array is compatible with any
190
// array type, so when the direct resolution fails, retry accepting
191
// each empty-array argument against any array parameter of a
192
// uniquely matching constructor. The empty array is then formatted
193
// with the parameter's element type in format_argument.
194
resolve_constructor_with_empty_arrays(
195
init_group: Semantic.Symbols.FUNCTION_GROUP,
196
positional: Collections.List[IR.Values.Value],
197
argument_types: Collections.List[Type]
198
) -> Semantic.Symbols.Function? is
199
let have_empty_array mut = false;
200
201
for argument in positional do
202
if is_empty_array(argument) then
203
have_empty_array = true;
204
fi
205
od
206
207
if !have_empty_array then
208
return null;
209
fi
210
211
let match: Semantic.Symbols.Function? mut = null;
212
213
for candidate in init_group.functions do
214
if candidate.arguments.count != positional.count then
215
continue;
216
fi
217
218
let ok mut = true;
219
220
for i in 0..positional.count do
221
let parameter = candidate.arguments[i];
222
223
if is_empty_array(positional[i]) then
224
if !parameter.get_element_type()? then
225
ok = false;
226
fi
227
elif !parameter.is_assignable_from(argument_types[i]) then
228
ok = false;
229
fi
230
od
231
232
if ok then
233
if match? then
234
return null;
235
fi
236
237
match = candidate;
238
fi
239
od
240
241
return match;
242
si
243
244
// ghūl cannot omit an optional parameter in an ordinary call,
245
// so a constructor called with fewer positional arguments
246
// than its arity — e.g. an attribute whose .NET constructor
247
// carries `CallerFilePath`/`CallerLineNumber` defaults —
248
// would otherwise need every trailing default spelled out at
249
// the call site. Retry accepting the supplied prefix against
250
// a uniquely matching constructor whose unsupplied trailing
251
// parameters all declare a default value; format_arguments
252
// fills those defaults in.
253
//
254
// Only a concrete literal default qualifies — the "default"
255
// sentinel (a ghūl source `= _` parameter, or a reflected
256
// parameter whose CLR default is itself `default(T)`) is
257
// excluded, since format_argument has no ilasm literal to
258
// render it as for an arbitrary parameter type.
259
resolve_constructor_with_omitted_optionals(
260
init_group: Semantic.Symbols.FUNCTION_GROUP,
261
argument_types: Collections.List[Type]
262
) -> Semantic.Symbols.Function? is
263
let match: Semantic.Symbols.Function? mut = null;
264
265
for candidate in init_group.functions do
266
if candidate.arguments.count <= argument_types.count then
267
continue;
268
fi
269
270
let ok mut = true;
271
272
for i in 0..argument_types.count do
273
if !candidate.arguments[i].is_assignable_from(argument_types[i]) then
274
ok = false;
275
fi
276
od
277
278
if ok then
279
for i in argument_types.count..candidate.arguments.count do
280
if
281
i >= candidate.argument_defaults.count \/
282
!candidate.argument_defaults[i]? \/
283
candidate.argument_defaults[i] =~ "default"
284
then
285
ok = false;
286
fi
287
od
288
fi
289
290
if ok then
291
if match? then
292
return null;
293
fi
294
295
match = candidate;
296
fi
297
od
298
299
return match;
300
si
301
302
is_empty_array(value: IR.Values.Value) -> bool is
303
if let sequence: IR.Values.SEQUENCE = value then
304
return sequence.values.count == 0;
305
fi
306
307
return false;
308
si
309
310
// The ilasm textual custom-attribute body: `( 01 00 00 00 )` when
311
// there are no arguments at all, otherwise `{ <fixed>... <named>... }`.
312
// Returns null if any argument is not a compile-time constant.
313
format_arguments(
314
pragma: Trees.Pragmas.PRAGMA,
315
attribute_type: Semantic.Symbols.Classy,
316
constructor: Semantic.Symbols.Function,
317
positional: Collections.List[IR.Values.Value]
318
) -> string? is
319
let named = pragma.named_arguments;
320
let have_named = named? /\ named.count > 0;
321
322
if constructor.arguments.count == 0 /\ !have_named then
323
return "( 01 00 00 00 )";
324
fi
325
326
let parameter_types = constructor.arguments;
327
328
let buffer = StringBuilder();
329
330
buffer.append("{{");
331
332
for i in 0..parameter_types.count do
333
let parameter_type = parameter_types[i];
334
335
// A trailing parameter beyond what was supplied only
336
// reaches here via resolve_constructor_with_omitted_
337
// optionals, which already guarantees a usable literal
338
// default at this index.
339
let argument =
340
if i < positional.count then
341
positional[i];
342
else
343
DEFAULT_ARGUMENT_VALUES.build(
344
constructor.argument_defaults[i],
345
parameter_type,
346
_innate_symbol_lookup
347
);
348
fi;
349
350
let formatted = format_argument(parameter_type, argument);
351
352
if !formatted? then
353
_logger.error(pragma.location, "attribute argument {i + 1} is not a compile-time constant");
354
return null;
355
fi
356
357
buffer.append(" ").append(formatted);
358
od
359
360
if have_named then
361
for named_argument in named! do
362
let formatted = format_named_argument(attribute_type, named_argument);
363
364
if !formatted? then
365
return null;
366
fi
367
368
buffer.append(" ").append(formatted);
369
od
370
fi
371
372
buffer.append(" }}");
373
374
return buffer.to_string();
375
si
376
377
// One `name = value` argument in the ilasm textual form —
378
// `property <type> 'Name' = <value>` or `field <type> 'Name' = ...`.
379
// Returns null (after logging) if the name does not resolve to a
380
// field or property of the attribute, or the value is unsupported.
381
format_named_argument(
382
attribute_type: Semantic.Symbols.Classy,
383
named_argument: Trees.Pragmas.NAMED_ARGUMENT
384
) -> string? is
385
let name = named_argument.name.to_string();
386
387
let member = attribute_type.find_member(name);
388
389
if !member? then
390
_logger.error(named_argument.name.location, "attribute {attribute_type.name} has no field or property {name}");
391
return null;
392
fi
393
394
let keyword: string mut;
395
396
if member.symbol_kind == Semantic.Symbols.SymbolKind.PROPERTY then
397
keyword = "property";
398
elif member.symbol_kind == Semantic.Symbols.SymbolKind.FIELD then
399
keyword = "field";
400
else
401
_logger.error(named_argument.name.location, "{name} is not a field or property of attribute {attribute_type.name}");
402
return null;
403
fi
404
405
let member_type = member.type;
406
407
if !member_type? then
408
_logger.error(named_argument.name.location, "attribute argument {name} has an unsupported type");
409
return null;
410
fi
411
412
let type_token = named_argument_type_token(member_type);
413
414
if !type_token? then
415
_logger.error(named_argument.name.location, "attribute argument {name} has an unsupported type");
416
return null;
417
fi
418
419
let formatted_value = format_argument(member_type, named_argument.value.value);
420
421
if !formatted_value? then
422
_logger.error(named_argument.value.location, "attribute argument {name} is not a compile-time constant");
423
return null;
424
fi
425
426
// The blob must carry the member's real .NET name. For a
427
// reflected attribute that is its IL name (`TypeDiscriminator-
428
// PropertyName`), not the snake_case identifier the call site
429
// uses (`type_discriminator_property_name`).
430
let il_member_name = if member.il_name_override? then member.il_name_override else name fi;
431
432
return "{keyword} {type_token} '{il_member_name}' = {formatted_value}";
433
si
434
435
// One positional or named-value argument in the ilasm textual
436
// form, or null if `value` is not a supported compile-time
437
// constant — a string, a number (optionally negated), a bool, an
438
// enum member, `null`, or an array of those.
439
format_argument(parameter_type: Type?, value: IR.Values.Value?) -> string? is
440
if !value? then
441
return null;
442
fi
443
444
if parameter_type? /\ parameter_type.matches(_innate_symbol_lookup.get_object_type()) then
445
let inner = format_typed_value(value);
446
447
if inner? then
448
return "object({inner})";
449
fi
450
451
return null;
452
fi
453
454
if isa IR.Values.NULL(value) then
455
if parameter_type? /\ parameter_type.matches(_innate_symbol_lookup.get_string_type()) then
456
return "string(nullref)";
457
fi
458
459
return null;
460
fi
461
462
if let string_value: IR.Values.Literal.STRING = value then
463
return "string({quote(string_value.value)})";
464
fi
465
466
let number_text = try_get_number_text(value);
467
468
if number_text? then
469
let keyword = serialization_keyword(parameter_type);
470
471
if keyword =~ "bool" then
472
return "bool({number_text_to_bool(number_text)})";
473
fi
474
475
return "{keyword}({number_text})";
476
fi
477
478
if let sequence: IR.Values.SEQUENCE = value then
479
// An empty array literal carried no elements to infer from
480
// and fell back to object[]; take its element type from the
481
// parameter instead.
482
if sequence.values.count == 0 /\ parameter_type? then
483
let element_type = parameter_type.get_element_type();
484
485
if element_type? then
486
let element_token = named_argument_type_token(element_type);
487
488
if element_token? then
489
return "{element_token}[0]()";
490
fi
491
fi
492
fi
493
494
return format_array(sequence);
495
fi
496
497
if let typeof_value: IR.Values.TYPEOF = value then
498
return "type({typeof_value.typeof_type.get_il_class_name().trim()})";
499
fi
500
501
return null;
502
si
503
504
// The ilasm `<element>[N](e0 e1 ...)` form for an array argument.
505
format_array(sequence: IR.Values.SEQUENCE) -> string? is
506
let element_token = named_argument_type_token(sequence.element_type);
507
508
if !element_token? then
509
return null;
510
fi
511
512
let buffer = StringBuilder();
513
514
buffer
515
.append(element_token)
516
.append("[")
517
.append(sequence.values.count)
518
.append("](");
519
520
for i in 0..sequence.values.count do
521
if i > 0 then
522
buffer.append(" ");
523
fi
524
525
let element = format_array_element(sequence.element_type, sequence.values[i]);
526
527
if !element? then
528
return null;
529
fi
530
531
buffer.append(element);
532
od
533
534
buffer.append(")");
535
536
return buffer.to_string();
537
si
538
539
// A single array element — the bare form, without the
540
// `keyword(...)` wrapper that a scalar argument carries. An
541
// element of an `object[]` is itself self-describing, so for
542
// an `object`-typed element type each element is emitted in its
543
// value-typed form (`int32(5)`, `string('x')`, …) rather than
544
// bare — that is what `format_typed_value` produces.
545
format_array_element(element_type: Type?, value: IR.Values.Value) -> string? is
546
if element_type? /\ element_type.matches(_innate_symbol_lookup.get_object_type()) then
547
return format_typed_value(value);
548
fi
549
550
if let string_value: IR.Values.Literal.STRING = value then
551
return quote(string_value.value);
552
fi
553
554
let number_text = try_get_number_text(value);
555
556
if number_text? then
557
if serialization_keyword(element_type) =~ "bool" then
558
return number_text_to_bool(number_text);
559
fi
560
561
return number_text;
562
fi
563
564
return null;
565
si
566
567
// The ilasm typed self-describing form for a value, used as the
568
// inner of an `object(...)` wrapper and as each element of an
569
// `object[]` array. Keys off the value's own type rather than
570
// any enclosing slot type, because that is exactly the
571
// information that has to be re-asserted inline when the slot's
572
// declared type is `System.Object`.
573
format_typed_value(value: IR.Values.Value) -> string? is
574
if isa IR.Values.NULL(value) then
575
// No top-level `nullref` form inside ilasm's `object(...)`;
576
// a null reference is encoded as a null string (the form
577
// C# emits for `[Foo((object)null)]`).
578
return "string(nullref)";
579
fi
580
581
if let string_value: IR.Values.Literal.STRING = value then
582
return "string({quote(string_value.value)})";
583
fi
584
585
let number_text = try_get_number_text(value);
586
587
if number_text? then
588
if value.type? then
589
let primitive = try_primitive_keyword(value.type!);
590
591
if primitive? then
592
if primitive =~ "bool" then
593
return "bool({number_text_to_bool(number_text)})";
594
fi
595
596
return "{primitive}({number_text})";
597
fi
598
599
// An enum-typed value inside an `object` slot would
600
// require encoding the enum's type-name SerString
601
// into the blob, and ilasm's `{...}` grammar has no
602
// production for `enum` inside `object(...)` or as
603
// an object-array element — the whole `.custom`
604
// would have to be emitted as raw bytes. Reject for
605
// now; the caller surfaces "not a compile-time
606
// constant".
607
fi
608
609
return null;
610
fi
611
612
if let typeof_value: IR.Values.TYPEOF = value then
613
return "type({typeof_value.typeof_type.get_il_class_name().trim()})";
614
fi
615
616
if let sequence: IR.Values.SEQUENCE = value then
617
return format_array(sequence);
618
fi
619
620
return null;
621
si
622
623
// The numeric text of a NUMBER literal, or of a unary minus
624
// applied to one — `-5` parses as a `-` innate over `5`, not as a
625
// negative literal — or null if `value` is not a numeric constant.
626
try_get_number_text(value: IR.Values.Value) -> string? is
627
if let number: IR.Values.Literal.NUMBER = value then
628
return number.value;
629
fi
630
631
if let innate_call: IR.Values.Call.INNATE = value then
632
if
633
innate_call.function.name =~ "-" /\
634
innate_call.arguments.count == 1
635
then
636
if let number: IR.Values.Literal.NUMBER = innate_call.arguments[0] then
637
return "-{number.value}";
638
fi
639
fi
640
fi
641
642
return null;
643
si
644
645
number_text_to_bool(number_text: string) -> string =>
646
if number_text =~ "0" then "false" else "true" fi;
647
648
// The ilasm type token for a named-argument member, an array
649
// member, or an array element — a primitive keyword, `string`,
650
// `enum <name>`, or `<element>[]`; null if the type cannot be
651
// encoded into a custom-attribute blob from a textual literal.
652
named_argument_type_token(type: Type?) -> string? is
653
if !type? then
654
return null;
655
fi
656
657
if type.matches(_innate_symbol_lookup.get_string_type()) then
658
return "string";
659
fi
660
661
if type.matches(_innate_symbol_lookup.get_type_type()) then
662
return "type";
663
fi
664
665
if type.matches(_innate_symbol_lookup.get_object_type()) then
666
return "object";
667
fi
668
669
let primitive = try_primitive_keyword(type);
670
671
if primitive? then
672
return primitive;
673
fi
674
675
if let array: Semantic.Types.ARRAY = type then
676
let element_token = named_argument_type_token(array.arguments[0]);
677
678
if element_token? then
679
return "{element_token}[]";
680
fi
681
682
return null;
683
fi
684
685
if type.symbol.symbol_kind == Semantic.Symbols.SymbolKind.ENUM then
686
if let classy: Semantic.Symbols.Classy = type.symbol then
687
if classy.il_assembly_name? then
688
return "enum [{classy.il_assembly_name}]{classy.qualified_name}";
689
fi
690
691
return "enum {classy.qualified_name}";
692
fi
693
fi
694
695
return null;
696
si
697
698
// The IL keyword for a primitive value type — the literal token
699
// ilasm expects in a custom-attribute blob — or null for anything
700
// that is not a primitive.
701
try_primitive_keyword(type: Type?) -> string? is
702
if !type? then
703
return null;
704
fi
705
706
let il = type.get_il_type().trim();
707
708
if
709
il =~ "bool" \/ il =~ "char" \/
710
il =~ "int8" \/ il =~ "uint8" \/
711
il =~ "int16" \/ il =~ "uint16" \/
712
il =~ "int32" \/ il =~ "uint32" \/
713
il =~ "int64" \/ il =~ "uint64" \/
714
il =~ "float32" \/ il =~ "float64"
715
then
716
return il;
717
fi
718
719
return null;
720
si
721
722
// The ilasm serialization keyword for a numeric or bool value:
723
// the parameter type's primitive keyword, or `int32` for an enum,
724
// whose fixed-argument form encodes the underlying value.
725
serialization_keyword(parameter_type: Type?) -> string is
726
let primitive = try_primitive_keyword(parameter_type);
727
728
if primitive? then
729
return primitive;
730
fi
731
732
return "int32";
733
si
734
735
quote(value: string) -> string =>
736
Semantic.DotNet.IL_ATTRIBUTE_ARGUMENTS.quote(value);
737
si
738
si