Skip to content
← Back

src/syntax/process/rewrite-syntax-trees/add_accessors_for_properties.ghul

1
namespace Syntax.Process is
2
use Source
3
use Trees
4
5
use Logging
6
7
use Collections.LIST
8
9
use Ghul.Pipes
10
11
// TODO rename this: it's not just for properties
12
class ADD_ACCESSORS_FOR_PROPERTIES: Visitor is
13
_pragma_scope_stack: PRAGMA_SCOPE_STACK
14
_stack: Collections.STACK[Definitions.LIST]
15
16
enclosing_definition: Definitions.LIST => _stack.peek()
17
18
init() is
19
super.init()
20
21
_pragma_scope_stack = PRAGMA_SCOPE_STACK()
22
_stack = Collections.STACK[Definitions.LIST]()
23
si
24
25
apply(root: Node) is
26
root.walk(self)
27
si
28
29
pre(pragma: Definitions.PRAGMA) -> bool is
30
_pragma_scope_stack.enter(pragma.pragma)
31
32
// `@equality` asks for the memberwise operator. It is read
33
// here rather than where pragmas become attributes,
34
// because the members it asks for are written in this
35
// pass, before the declaration has a symbol to carry the
36
// request on.
37
if pragma.is_name_equal_to("equality") then
38
let `class = cast Definitions.CLASS?(pragma.without_pragmas)
39
40
if `class? then
41
`class.wants_equality = true
42
fi
43
fi
44
45
return false
46
si
47
48
visit(pragma: Definitions.PRAGMA) is
49
_pragma_scope_stack.leave(pragma.pragma)
50
si
51
52
pre(property: Definitions.PROPERTY) -> bool is
53
// The parser synthesises a `Bodies.NULL` read_body for properties
54
// declared in a trait without an explicit body (the abstract-method
55
// marker), so test for a *real* body here — otherwise this fires
56
// spuriously for `field x: T;` in a trait, swallowing the proper
57
// "field is not valid here" diagnostic from declare-symbols.
58
let has_real_body =
59
(property.read_body? /\ !isa Bodies.NULL(property.read_body)) \/
60
property.assign_body? \/
61
property.assign_argument?
62
63
if property.modifiers.is_field /\ has_real_body then
64
IoC.CONTAINER.instance.logger
65
.error(property.modifiers.storage_class!.location, "a field cannot have body")
66
property.modifiers.clear_storage_class()
67
fi
68
69
let name = property.name
70
71
// `private` on a body declaration has no member to rename
72
// onto — unlike the primary-constructor parameter form, which
73
// the rewriter turns into an underscore-named capture before
74
// this pass ever sees it. Left alone, a plain-named private
75
// property would fall through with no auto-property and no
76
// real body, giving empty accessors that silently discard
77
// writes and read back the default. Reject it and point at
78
// the working spelling.
79
if
80
property.modifiers.is_private /\ name? /\
81
!name.name.starts_with('_') /\ !has_real_body
82
then
83
IoC.CONTAINER.instance.logger.error(
84
property.modifiers.access_modifier!.location,
85
"private is not valid on a body declaration",
86
name.location,
87
"help: name the member _{name.name} instead"
88
)
89
property.modifiers.clear_access_modifier()
90
fi
91
92
if
93
_stack.count > 0 /\
94
(!property.is_poisoned \/ _is_recoverable(property)) /\
95
!property.modifiers.is_field /\ name? /\ (
96
!name.name.starts_with('_') \/
97
property.read_body? \/
98
property.assign_body?
99
)
100
then
101
let is_assignable = !property.read_body? \/ property.assign_argument?
102
103
add_accessor_functions_for_property(property, is_assignable)
104
fi
105
106
return true
107
si
108
109
// A declaration the parser gave up on after its name and type still
110
// reads as an ordinary auto-property when it has no accessor bodies,
111
// and as a read-only one when all it has is a read body - a damaged
112
// function header whose body the parser kept - so that the body is
113
// compiled and checked like any other.
114
_is_recoverable(property: Definitions.PROPERTY) -> bool =>
115
!property.assign_body? /\ !property.type_expression.is_poisoned
116
117
add_accessor_functions_for_property(property: Definitions.PROPERTY, is_assignable: bool) is
118
// gated on a present name by the caller
119
let property_name = property.name!
120
121
let read_name =
122
Identifiers.Identifier(
123
property_name.location,
124
"$get_{property_name.name}"
125
)
126
127
property.is_auto_property =
128
!property.read_body? /\
129
!property.assign_body? /\
130
!property_name.name.starts_with('_') /\
131
!property.modifiers.is_private
132
133
let backing_variable_name = "${property_name.name}"
134
135
let assign_argument_name mut = "$$value"
136
137
if property.assign_argument != null then
138
assign_argument_name = property.assign_argument.name
139
fi
140
141
if property.is_auto_property then
142
let backing_variable =
143
Variables.VARIABLE(
144
property_name.location,
145
Identifiers.Identifier(
146
LOCATION.internal,
147
backing_variable_name
148
),
149
property.type_expression.copy(),
150
property.modifiers.is_static,
151
true,
152
null
153
)
154
155
backing_variable.mark_synthesized()
156
157
enclosing_definition.add(backing_variable)
158
159
// The synthesised accessor bodies have no user source —
160
// BLOCK + Statements.LIST locations are internal so the
161
// incremental body re-walk's BODY_SPANS skips them and
162
// does not treat the property declaration itself (at
163
// `property.location`) as "inside" its own getter body.
164
// Inner statements keep `property.location` so any later
165
// diagnostic against the synthesised RETURN / ASSIGNMENT
166
// still anchors on the property declaration the user
167
// wrote.
168
property.read_body =
169
Trees.Bodies.BLOCK(LOCATION.internal,
170
Trees.Statements.LIST(LOCATION.internal,
171
Collections.LIST[Statements.Statement]([
172
Trees.Statements.RETURN(property.location,
173
Trees.Expressions.IDENTIFIER(property.location,
174
Trees.Identifiers.Identifier(property.location, backing_variable_name)
175
)
176
)
177
]: Statements.Statement)
178
)
179
)
180
181
property.assign_body =
182
Trees.Bodies.BLOCK(LOCATION.internal,
183
Trees.Statements.LIST(LOCATION.internal,
184
Collections.LIST[Statements.Statement]([
185
Trees.Statements.ASSIGNMENT(property.location,
186
Trees.Expressions.SIMPLE_LEFT_EXPRESSION(property.location,
187
Trees.Expressions.IDENTIFIER(property.location,
188
Trees.Identifiers.Identifier(property.location, backing_variable_name)
189
)
190
),
191
Trees.Expressions.IDENTIFIER(property.location,
192
Trees.Identifiers.Identifier(property.location, assign_argument_name)
193
)
194
)
195
]: Statements.Statement)
196
)
197
)
198
fi
199
200
// A write-only property (an assign body with no read body,
201
// as in a setter-only trigger property) has nothing to
202
// build a getter from.
203
if property.read_body? then
204
let read_function = Definitions.FUNCTION(
205
property.location,
206
read_name,
207
TypeExpressions.LIST(LOCATION.internal, Collections.LIST[TypeExpressions.TypeExpression](0)),
208
Variables.LIST(property_name.location, Collections.LIST[Variables.VARIABLE](0)),
209
property.type_expression.copy(),
210
property.modifiers.copy(),
211
property.read_body!
212
)
213
214
read_function.for_property = property
215
read_function.is_underscore_scoped = property_name.name.starts_with('_')
216
217
enclosing_definition.add(
218
read_function
219
)
220
221
property.read_function = read_function
222
fi
223
224
if is_assignable \/ property.is_auto_property then
225
let assign_name =
226
Identifiers.Identifier(
227
property_name.location,
228
"$set_{property_name.name}"
229
)
230
231
let assign_argument_is_synthesized = !property.assign_argument?
232
233
if !property.assign_argument? then
234
property.assign_argument = Identifiers.Identifier(property.location, "$$value")
235
fi
236
237
let assign_argument_variable =
238
Variables.VARIABLE(
239
property.assign_argument!.location,
240
property.assign_argument!,
241
property.type_expression.copy(),
242
false,
243
true,
244
null
245
)
246
247
if assign_argument_is_synthesized then
248
assign_argument_variable.mark_synthesized()
249
fi
250
251
let assign_function = Definitions.FUNCTION(
252
property_name.location,
253
assign_name,
254
TypeExpressions.LIST(LOCATION.internal, Collections.LIST[TypeExpressions.TypeExpression](0)),
255
Variables.LIST(LOCATION.internal, Collections.LIST[Variables.VARIABLE]([
256
assign_argument_variable
257
]:Variables.VARIABLE)),
258
TypeExpressions.NAMED(
259
LOCATION.internal,
260
Identifiers.Identifier(
261
LOCATION.internal,
262
"void"
263
)
264
),
265
property.modifiers.copy(),
266
property.assign_body!
267
)
268
269
assign_function.for_property = property
270
assign_function.is_assign_accessor = true
271
assign_function.is_underscore_scoped = property_name.name.starts_with('_')
272
273
enclosing_definition.add(
274
assign_function
275
)
276
277
property.assign_function = assign_function
278
fi
279
si
280
281
visit(property: Definitions.PROPERTY) is
282
si
283
284
pre(indexer: Definitions.INDEXER) -> bool is
285
// An indexer at the root of the tree belongs to no type - only
286
// a file whose mix of namespaces and globals was just rejected
287
// can produce one, and there is no enclosing definition to
288
// attach accessors to.
289
if _stack.count == 0 then
290
return true
291
fi
292
293
let name: string mut
294
let location: LOCATION mut
295
296
if indexer.name? then
297
name = indexer.name.name
298
location = indexer.name.location
299
else
300
name = "Item"
301
location = indexer.location
302
fi
303
304
if indexer.read_body? then
305
let read_accessor = Definitions.FUNCTION(
306
indexer.location,
307
Identifiers.Identifier(
308
location,
309
"get_{name}"
310
),
311
TypeExpressions.LIST(LOCATION.internal, Collections.LIST[TypeExpressions.TypeExpression](0)),
312
Variables.LIST(
313
LOCATION.internal,
314
Collections.LIST[Variables.VARIABLE]([indexer.index_argument.copy()]:Variables.VARIABLE)),
315
indexer.type_expression.copy(),
316
indexer.modifiers.copy(),
317
indexer.read_body!
318
)
319
read_accessor.for_indexer = indexer
320
enclosing_definition.add(read_accessor)
321
fi
322
323
if indexer.assign_body? then
324
let assign_accessor = Definitions.FUNCTION(
325
indexer.location,
326
Identifiers.Identifier(
327
location,
328
"set_{name}"
329
),
330
TypeExpressions.LIST(LOCATION.internal, Collections.LIST[TypeExpressions.TypeExpression](0)),
331
Variables.LIST(LOCATION.internal, Collections.LIST[Variables.VARIABLE]([
332
indexer.index_argument.copy(),
333
Variables.VARIABLE(
334
indexer.assign_argument!.location,
335
indexer.assign_argument!.copy(),
336
indexer.type_expression.copy(),
337
false,
338
true,
339
null
340
)
341
]:Variables.VARIABLE)),
342
TypeExpressions.NAMED(
343
LOCATION.internal,
344
Identifiers.Identifier(
345
LOCATION.internal,
346
"void"
347
)
348
),
349
indexer.modifiers.copy(),
350
indexer.assign_body!
351
)
352
assign_accessor.for_indexer = indexer
353
assign_accessor.is_assign_accessor = true
354
enclosing_definition.add(assign_accessor)
355
fi
356
357
return true
358
si
359
360
pre(`class: Definitions.CLASS) -> bool is
361
_stack.push(`class.body)
362
363
// A class declaring neither operator compares by reference
364
// and has no `=~` at all, so synthesizing one gives it the
365
// memberwise comparison a reader of the fields expects.
366
// Eligibility is answered from the class's own body here;
367
// what it inherits is not known until ancestors resolve,
368
// and SYNTHESIZE_CLASS_EQUALITY settles that later -
369
// withdrawing these members where an inherited operator
370
// answers already, and joining them to the base's where
371
// the base is synthesized too.
372
if !`class.is_poisoned /\ `class.wants_equality /\ declares_no_equality(`class.body) then
373
`class.body.add(_synthesized(get_memberwise_equals_for_class(`class)))
374
375
// A class holding nothing of its own gets no hash and
376
// no bridge. Either it compares by reference, which is
377
// what `object` already hashes and answers by, or it
378
// extends a class that holds members and inherits
379
// both, which hash exactly what its comparison reads.
380
if get_member_read_names(`class.body).count > 0 then
381
`class.body.add(_synthesized(get_memberwise_hash_code(`class.body)))
382
fi
383
fi
384
385
add_object_equals_bridge(`class)
386
387
return false
388
si
389
390
// A type that declares `=~` gets the `Object.Equals` bridge so
391
// .NET reaches the operator the user wrote. Skipped when the
392
// body already supplies that method, which is then the
393
// author's own bridge and is left alone.
394
//
395
// The bridge also requires the type to declare its own
396
// `get_hash_code`. .NET pairs the two - values that compare
397
// equal must hash equal, and a hash-based collection consults
398
// the hash first - and a type that declares neither is
399
// consistent as it stands, comparing and hashing by identity.
400
// Bridging on its own would break that pair rather than
401
// complete it: equality would answer by the operator while the
402
// hash still answered by identity, so two equal keys would
403
// land in different buckets. The hash cannot be synthesised
404
// either, since an operator is free to be coarser than the
405
// fields it reads and a memberwise hash would then disagree
406
// with it. So the type is left alone and the missing half is
407
// reported.
408
add_object_equals_bridge(classy: Definitions.Classy) is
409
if
410
classy.is_poisoned \/
411
!declares_function(classy.body, "=~") \/
412
declares_object_equals(classy.body)
413
then
414
return
415
fi
416
417
if !declares_function(classy.body, "get_hash_code") then
418
// A synthesized operator without a hash is the
419
// reference-identity one, which `object`'s own hash
420
// already pairs with. Nothing is out of step, so there
421
// is nothing to report.
422
if declares_synthesized_function(classy.body, "=~") then
423
return
424
fi
425
426
IoC.CONTAINER.instance.logger.warn(
427
classy.name.location,
428
"equality-without-hash",
429
"{classy.name.name} defines =~ but no get_hash_code, so .NET comparisons will not use the operator"
430
)
431
432
return
433
fi
434
435
classy.body.add(
436
get_object_equals_bridge(
437
LOCATION.internal,
438
get_classy_type_expression(classy)
439
)
440
)
441
si
442
443
visit(`class: Definitions.CLASS) is
444
_stack.pop()
445
si
446
447
pre(`trait: Definitions.TRAIT) -> bool is
448
_stack.push(`trait.body)
449
450
return false
451
si
452
453
visit(`trait: Definitions.TRAIT) is
454
_stack.pop()
455
si
456
457
// Members synthesized for a partial/impl block's properties -
458
// accessor functions, auto-property backing variables - belong
459
// in the block's own body, so that DECLARE_MEMBERS declares
460
// them into the block's target type. Without this the block's
461
// list never reaches the stack and they land in the enclosing
462
// namespace's body, where they are declared as globals.
463
pre(`partial: Definitions.PARTIAL) -> bool is
464
_stack.push(`partial.body)
465
466
return false
467
si
468
469
visit(`partial: Definitions.PARTIAL) is
470
_stack.pop()
471
si
472
473
pre(`impl: Definitions.IMPL) -> bool is
474
_stack.push(`impl.body)
475
476
return false
477
si
478
479
visit(`impl: Definitions.IMPL) is
480
_stack.pop()
481
si
482
483
pre(`struct: Definitions.STRUCT) -> bool is
484
_stack.push(`struct.body)
485
486
// A struct has no equality of its own to inherit and `==`
487
// does not apply to one, so a struct declaring neither
488
// operator has no equality at all until one is synthesized
489
// here. The bridge runs afterwards and finds the pair,
490
// which is what makes .NET reach the same comparison.
491
if !`struct.is_poisoned /\ wants_synthesized_equality(`struct.body) then
492
`struct.body.add(_synthesized(get_memberwise_equals_for_struct(`struct)))
493
`struct.body.add(_synthesized(get_memberwise_hash_code(`struct.body)))
494
fi
495
496
add_object_equals_bridge(`struct)
497
498
return false
499
si
500
501
visit(`struct: Definitions.STRUCT) is
502
_stack.pop()
503
si
504
505
pre(`union: Definitions.UNION) -> bool is
506
_stack.push(`union.body)
507
508
if `union.is_poisoned then
509
return false
510
fi
511
512
let unit_variant_count mut = 0
513
let non_unit_variant_count mut = 0
514
let default_variants = Collections.LIST[Definitions.VARIANT]()
515
516
for definition in `union.body do
517
if isa Definitions.VARIANT(definition) then
518
let variant = definition
519
520
let own_field_count =
521
variant.fields
522
|> filter(f => !f.is_inherited_primary)
523
|> count()
524
525
if own_field_count > 0 then
526
non_unit_variant_count = non_unit_variant_count + 1
527
else
528
unit_variant_count = unit_variant_count + 1
529
fi
530
531
if variant.is_default then
532
default_variants.add(variant)
533
fi
534
fi
535
od
536
537
if non_unit_variant_count == 0 /\ unit_variant_count == 0 then
538
IoC.CONTAINER.instance.logger
539
.error(`union.location, "union must have at least one variant")
540
fi
541
542
// Default-variant validation. The actual pick used by
543
// `compile_access` for `?`/`!` lives on `Symbols.UNION`,
544
// set during declare_members once the variant symbols
545
// exist. Here we just diagnose obvious mistakes that
546
// would otherwise produce a surprising default later.
547
if default_variants.count > 1 then
548
for d in default_variants do
549
let others = LIST[RELATED_LOCATION]()
550
551
for other in default_variants do
552
if other != d then
553
others.add(RELATED_LOCATION(other.location, "also marked default here"))
554
fi
555
od
556
557
IoC.CONTAINER.instance.logger
558
.error(d.location, "a union can have at most one default variant", others)
559
od
560
elif default_variants.count == 1 then
561
let d = default_variants[0]
562
563
let own_field_count =
564
d.fields
565
|> filter(f => !f.is_inherited_primary)
566
|> count()
567
568
if own_field_count == 0 then
569
IoC.CONTAINER.instance.logger
570
.error(d.location, "a default variant must hold at least one value")
571
fi
572
fi
573
574
// Synthesise structural equality on the union and its
575
// variants. The union's `=~` returns false (the runtime
576
// type is always some variant; this base is purely
577
// overridden). Each variant overrides `=~` with an isa
578
// check + field-by-field `object.equals`, the latter of
579
// which gives value semantics for primitives via boxing,
580
// for strings, and recursively for other unions whose
581
// synthesised `equals(object?)` we add here too.
582
// Synthesised `equals(object?)` makes the union a sound
583
// dictionary key; `get_hash_code` is required by the
584
// .NET equality contract (equal objects must hash equal).
585
`union.body.add(_synthesized(get_typed_equals_method_for_union(`union)))
586
`union.body.add(_synthesized(get_object_equals_method_for_union(`union)))
587
`union.body.add(_synthesized(get_hash_code_method_for_union()))
588
589
for definition in `union.body do
590
if isa Definitions.VARIANT(definition) then
591
let variant = definition
592
593
variant.body.add(_synthesized(get_typed_equals_method_for_variant(variant, `union)))
594
variant.body.add(_synthesized(get_hash_code_method_for_variant(variant)))
595
fi
596
od
597
598
return false
599
si
600
601
visit(`union: Definitions.UNION) is
602
_stack.pop()
603
si
604
605
// These carry the union's or variant's own declaration location
606
// rather than an internal one, so nothing else distinguishes
607
// them from a member the user wrote there.
608
_synthesized(function: Definitions.FUNCTION) -> Definitions.FUNCTION static is
609
function.mark_synthesized()
610
611
return function
612
si
613
614
pre(variant: Trees.Definitions.VARIANT) -> bool is
615
super.pre(variant)
616
_stack.push(variant.body)
617
return false
618
si
619
620
visit(variant: Trees.Definitions.VARIANT) is
621
if variant.is_poisoned then
622
_stack.pop()
623
return
624
fi
625
626
let init_function = get_init_method_for_variant(variant)
627
628
variant.body.add(init_function)
629
630
_stack.pop()
631
si
632
633
pre(`namespace: Definitions.NAMESPACE) -> bool is
634
_stack.push(`namespace.body)
635
636
return false
637
si
638
639
visit(`namespace: Definitions.NAMESPACE) is
640
_stack.pop()
641
si
642
si
643
si