Skip to content
← Back

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

1
namespace Syntax.Process is
2
use Source
3
use Logging
4
use Trees
5
6
// Synthesis helpers: capture matching, pragma rewrapping,
7
// auto-property and deconstruct generation, variant splice
8
// expansion and secondary-init chaining. The walk itself is in
9
// rewrite_primary_constructors.ghul.
10
partial REWRITE_PRIMARY_CONSTRUCTORS is
11
_strip_orphan_super_calls(classy: Trees.Definitions.Classy) is
12
let kept = Collections.LIST[Trees.Definitions.Definition]()
13
let any_dropped mut = false
14
15
for d in classy.body do
16
if isa Trees.Definitions.SUPER_CALL(d) then
17
_logger.error(
18
d.location,
19
"super(...) declaration requires a primary constructor header"
20
)
21
any_dropped = true
22
elif _is_unmatched_capture_shorthand(d) then
23
// A name on its own is the capture shorthand, and there
24
// is no header for it to capture from - so it has
25
// neither a type nor an accessor, and nothing
26
// downstream can make sense of it. Say so here, where
27
// the shape is still recognisable.
28
_logger.error(
29
d.location,
30
"capture shorthand requires a primary constructor header"
31
)
32
any_dropped = true
33
else
34
if _has_init_modifier(d) then
35
_logger.error(
36
d.location,
37
"init modifier is only valid on a primary constructor parameter"
38
)
39
fi
40
kept.add(d)
41
fi
42
od
43
44
if any_dropped then
45
classy.body.clear_definitions()
46
47
for d in kept do
48
classy.body.add(d)
49
od
50
fi
51
si
52
53
_is_unmatched_capture_shorthand(d: Trees.Definitions.Definition) -> bool is
54
if !isa Trees.Definitions.PROPERTY(d) then
55
return false
56
fi
57
58
let p = cast Trees.Definitions.PROPERTY(d)
59
60
if p.read_body? \/ p.assign_body? then
61
return false
62
fi
63
64
return p.name? /\ p.type_expression.is_inferred
65
si
66
67
// Every @pragma(...) wrapping `d`, outermost first, unwrapping
68
// as many levels as were stacked - a declaration can carry more
69
// than one.
70
_collect_pragmas(d: Trees.Definitions.Definition) -> Collections.LIST[Trees.Definitions.PRAGMA] is
71
let result = Collections.LIST[Trees.Definitions.PRAGMA]()
72
let current: Trees.Definitions.Definition mut = d
73
74
while isa Trees.Definitions.PRAGMA(current) do
75
let p = cast Trees.Definitions.PRAGMA(current)
76
77
result.add(p)
78
current = p.definition
79
od
80
81
return result
82
si
83
84
// Rebuilds the nesting `_collect_pragmas` unwrapped, around a
85
// freshly synthesised definition in place of the original one.
86
_rewrap_pragmas(
87
pragmas: Collections.LIST[Trees.Definitions.PRAGMA],
88
inner: Trees.Definitions.Definition
89
) -> Trees.Definitions.Definition is
90
let result: Trees.Definitions.Definition mut = inner
91
let i mut = pragmas.count - 1
92
93
while i >= 0 do
94
let p = pragmas[i]
95
96
result = Trees.Definitions.PRAGMA(p.pragma.location :: result.location, p.pragma, result)
97
i = i - 1
98
od
99
100
return result
101
si
102
103
_is_init_function(d: Trees.Definitions.Definition) -> bool is
104
if !isa Trees.Definitions.FUNCTION(d) then
105
return false
106
fi
107
108
let f = cast Trees.Definitions.FUNCTION(d)
109
110
return f.name? /\ f.name.name =~ "init"
111
si
112
113
_is_primary_init(f: Trees.Definitions.FUNCTION) -> bool is
114
// init(..) — exactly one parameter and it's the splice marker.
115
let args = f.arguments.variables
116
117
return args.count == 1 /\ args[0].is_splice
118
si
119
120
_arg_list_has_splice(args: Trees.Variables.LIST?) -> bool is
121
if !args? then
122
return false
123
fi
124
125
for v in args do
126
if v.is_splice then
127
return true
128
fi
129
od
130
131
return false
132
si
133
134
_count_splices(args: Trees.Variables.LIST?) -> int is
135
if !args? then
136
return 0
137
fi
138
139
let n mut = 0
140
141
for v in args do
142
if v.is_splice then
143
n = n + 1
144
fi
145
od
146
147
return n
148
si
149
150
_strip_underscore(name: string) -> string is
151
if name.length > 0 /\ name[0] == '_' then
152
return name.substring(1)
153
fi
154
155
return name
156
si
157
158
// Diagnoses incoherent combinations on a primary-ctor parameter:
159
// - `init` combined with any visibility modifier or with another
160
// storage modifier means "no field, but also make the (non-
161
// existent) field public / a plain field" — incoherent.
162
// - `init` combined with a `_`-prefixed name means "no field, but
163
// the name implies a private field" — same incoherence.
164
// The rewriter only flags; it doesn't strip, so the user sees
165
// every diagnostic in one pass.
166
_validate_primary_param_modifiers(p: Trees.Variables.VARIABLE) is
167
let modifiers = p.modifiers
168
169
if !modifiers? then
170
return
171
fi
172
173
if modifiers.is_init then
174
if let modifiers.access_modifier? then
175
_logger.error(
176
access_modifier.location,
177
"init modifier cannot combine with visibility modifiers"
178
)
179
fi
180
if let
181
modifiers.storage_class? /\
182
!storage_class.is_init
183
then
184
_logger.error(
185
storage_class.location,
186
"init modifier cannot combine with other storage modifiers"
187
)
188
fi
189
if let
190
p.name? /\
191
name.name.length > 0 /\
192
name.name[0] == '_'
193
then
194
_logger.error(
195
p.location,
196
"init modifier cannot combine with a _-prefixed parameter name"
197
)
198
fi
199
fi
200
si
201
202
// Body decls with the `init` modifier are diagnosed at the body-
203
// iteration step. This helper recognises the (FUNCTION / PROPERTY)
204
// shapes that carry modifier lists.
205
_has_init_modifier(d: Trees.Definitions.Definition) -> bool is
206
if isa Trees.Definitions.PROPERTY(d) then
207
let p = cast Trees.Definitions.PROPERTY(d)
208
return p.modifiers.is_init
209
fi
210
if isa Trees.Definitions.FUNCTION(d) then
211
let f = cast Trees.Definitions.FUNCTION(d)
212
return f.modifiers.is_init
213
fi
214
return false
215
si
216
217
// Synthesise a body-level field/property declaration mirroring the
218
// primary-ctor parameter, ready to flow through
219
// `add_accessors_for_properties` and `declare_symbols` as if the
220
// user had written it by hand. Strips the `init` modifier from
221
// the synthesised modifier list as a safety net — even though the
222
// caller has already filtered INIT params, leaving INIT on the
223
// body decl would re-trigger the "init only on primary param"
224
// diagnostic.
225
_synthesise_auto_property(p: Trees.Variables.VARIABLE) -> Trees.Definitions.PROPERTY =>
226
let loc = p.location in
227
let modifiers = _make_body_modifiers(p) in
228
Trees.Definitions.PROPERTY(
229
loc,
230
p.type_expression.copy(),
231
_body_property_name(p),
232
modifiers,
233
null,
234
null,
235
null
236
)
237
238
// `private` names the member the way the naming convention does:
239
// `v: int private` captures into `_v`, leaving the parameter
240
// itself as `v`. The underscore is what the later passes read for
241
// visibility, so `_make_body_modifiers` drops the modifier rather
242
// than carrying both spellings of the same fact.
243
_body_property_name(p: Trees.Variables.VARIABLE) -> Trees.Identifiers.Identifier is
244
let name = p.name!
245
246
if _is_private_param(p) /\ !name.name.starts_with('_') then
247
return Trees.Identifiers.Identifier(name.location, "_{name.name}")
248
fi
249
250
return name.copy()
251
si
252
253
_is_private_param(p: Trees.Variables.VARIABLE) -> bool is
254
if let p.modifiers?, modifiers.access_modifier? /\ access_modifier.is_private then
255
return true
256
fi
257
258
return false
259
si
260
261
_make_body_modifiers(p: Trees.Variables.VARIABLE) -> Trees.Modifiers.LIST is
262
let loc = p.location
263
let modifiers = p.modifiers
264
265
if !modifiers? then
266
return Trees.Modifiers.LIST(loc, null, null)
267
fi
268
269
let access: Trees.Modifiers.AccessModifier? mut = null
270
let storage: Trees.Modifiers.StorageClass? mut = null
271
272
if let modifiers.access_modifier? /\ !access_modifier.is_private then
273
access = access_modifier.copy()
274
fi
275
276
// Drop INIT — it has no meaning on the body decl. Other
277
// storage classes (FIELD, etc.) flow through.
278
if let modifiers.storage_class? /\ !storage_class.is_init then
279
storage = storage_class.copy()
280
fi
281
282
return Trees.Modifiers.LIST(loc, access, storage)
283
si
284
285
// See `DESTRUCTURE_RESOLVER` for the precedence the synthesised
286
// deconstruct slots into. Guard preconditions:
287
// - User wrote no `deconstruct` of any arity in this class body.
288
// - User exposed no backtick-numeric property (`0`, `1`, ...).
289
// - At least one public-readable capture exists to expose.
290
// The synthesised method is `public`, instance, with one `T ref`
291
// parameter per included capture (named after the capture so the
292
// body reads naturally; the destructure call site only ever
293
// matches by arity + ref-ness).
294
_maybe_synthesise_deconstruct(
295
classy: Trees.Definitions.Classy,
296
class_location: LOCATION,
297
primary_params: Trees.Variables.LIST,
298
param_for_capture: Collections.MAP[Trees.Definitions.PROPERTY, Trees.Variables.VARIABLE],
299
already_captured_param_names: Collections.SET[string],
300
kept: Collections.MutableList[Trees.Definitions.Definition]
301
) is
302
for d in classy.body do
303
if isa Trees.Definitions.FUNCTION(d) then
304
let f = cast Trees.Definitions.FUNCTION?(d)!
305
if f.name? /\ f.name.name =~ "deconstruct" then
306
return
307
fi
308
elif isa Trees.Definitions.PROPERTY(d) then
309
let p = cast Trees.Definitions.PROPERTY?(d)!
310
if p.name? /\ Semantic.Symbols.Symbol.is_positional_member_name(p.name.name) then
311
return
312
fi
313
fi
314
od
315
316
// Reverse-lookup table: primary-ctor parameter name -> its
317
// capture property. Built once so the capture-collection loop
318
// below stays O(captures).
319
let capture_for_param_name = Collections.MAP[string, Trees.Definitions.PROPERTY]()
320
for kvp in param_for_capture do
321
let cap = kvp.key
322
let param = kvp.value
323
if let param.name? then
324
capture_for_param_name[name.name] = cap
325
fi
326
od
327
328
let deconstruct_captures = Collections.LIST[Trees.Definitions.PROPERTY]()
329
for p in primary_params do
330
let name = p.name
331
if !name? then
332
continue
333
fi
334
if !already_captured_param_names.contains(name.name) then
335
continue
336
fi
337
if !capture_for_param_name.contains_key(name.name) then
338
continue
339
fi
340
let capture = capture_for_param_name[name.name]
341
if !_is_public_readable_capture(capture) then
342
continue
343
fi
344
deconstruct_captures.add(capture)
345
od
346
347
if deconstruct_captures.count == 0 then
348
return
349
fi
350
351
let deconstruct = _synthesise_deconstruct(class_location, deconstruct_captures)
352
353
// Wrap in an @IL.name("Deconstruct") pragma so the method
354
// satisfies the standard .NET `Deconstruct` contract used by
355
// C# positional patterns and other cross-language consumers.
356
kept.add(_wrap_in_il_name_pragma(class_location, "Deconstruct", deconstruct))
357
si
358
359
_is_public_readable_capture(p: Trees.Definitions.PROPERTY) -> bool is
360
// ghūl carries read-visibility two ways: an explicit
361
// access modifier, or a leading `_` on the name (the
362
// convention: "_x" -> protected for reading). Both must
363
// be checked — the synthesised deconstruct is `public`,
364
// and surfacing a protected member through it would
365
// bypass the encapsulation the user signalled.
366
if let p.modifiers?, modifiers.access_modifier? /\ access_modifier.is_private then
367
return false
368
fi
369
if p.name? /\ p.name.name.length > 0 /\ p.name.name[0] == '_' then
370
return false
371
fi
372
return true
373
si
374
375
_synthesise_deconstruct(
376
class_location: LOCATION,
377
captures: Collections.List[Trees.Definitions.PROPERTY]
378
) -> Trees.Definitions.FUNCTION is
379
let arg_vars = Collections.LIST[Trees.Variables.VARIABLE]()
380
let body_stmts = Collections.LIST[Trees.Statements.Statement]()
381
382
for capture in captures do
383
let cap_loc = capture.location
384
let arg_type =
385
Trees.TypeExpressions.REFERENCE(
386
cap_loc,
387
capture.type_expression.copy()
388
)
389
let arg =
390
Trees.Variables.VARIABLE(
391
cap_loc,
392
capture.name!.copy(),
393
arg_type,
394
false,
395
true,
396
null
397
)
398
arg.mark_argument()
399
arg_vars.add(arg)
400
401
body_stmts.add(_make_deconstruct_assignment(capture))
402
od
403
404
let arg_list = Trees.Variables.LIST(class_location, arg_vars)
405
let body =
406
Trees.Bodies.BLOCK(
407
LOCATION.internal,
408
Trees.Statements.LIST(LOCATION.internal, body_stmts)
409
)
410
let modifiers =
411
Trees.Modifiers.LIST(
412
class_location,
413
Trees.Modifiers.PUBLIC(class_location),
414
null
415
)
416
let deconstruct =
417
Trees.Definitions.FUNCTION(
418
class_location,
419
Trees.Identifiers.Identifier(LOCATION.internal, "deconstruct"),
420
Trees.TypeExpressions.LIST(LOCATION.internal, Collections.LIST[Trees.TypeExpressions.TypeExpression](0)),
421
arg_list,
422
Trees.TypeExpressions.INFER(class_location),
423
modifiers,
424
body
425
)
426
deconstruct.mark_synthesized()
427
428
return deconstruct
429
si
430
431
_make_deconstruct_assignment(
432
capture: Trees.Definitions.PROPERTY
433
) -> Trees.Statements.Statement is
434
let loc = capture.location
435
436
// <capture-name>! = self.<capture-name>;
437
//
438
// Postfix `!` on a `T ref` LHS lowers to a `stobj <T>` —
439
// the write-through-ref form. Without it,
440
// `<capture-name> = self.<...>` would error with
441
// "Ghul.T is not assignable to Ghul.T ref".
442
let arg_ref =
443
Trees.Expressions.IDENTIFIER(loc, capture.name!.copy())
444
let unwrap = Trees.Expressions.UNWRAP(loc, arg_ref)
445
let left = Trees.Expressions.SIMPLE_LEFT_EXPRESSION(loc, unwrap)
446
447
let self_expr = Trees.Expressions.SELF(loc)
448
let right =
449
Trees.Expressions.MEMBER(
450
loc,
451
self_expr,
452
capture.name!.copy(),
453
loc
454
)
455
456
return Trees.Statements.ASSIGNMENT(loc, left, right)
457
si
458
459
_wrap_in_il_name_pragma(
460
loc: LOCATION,
461
il_name: string,
462
definition: Trees.Definitions.Definition
463
) -> Trees.Definitions.PRAGMA is
464
let name_literal = Trees.Expressions.Literals.STRING(loc, il_name)
465
let args = Collections.LIST[Trees.Expressions.Expression]()
466
args.add(name_literal)
467
let pragma =
468
Trees.Pragmas.PRAGMA(
469
loc,
470
Trees.Identifiers.Identifier(loc, "IL.name"),
471
Trees.Expressions.LIST(loc, args),
472
null
473
)
474
return Trees.Definitions.PRAGMA(loc, pragma, definition)
475
si
476
477
_make_super_init_call(
478
super_call: Trees.Definitions.SUPER_CALL
479
) -> Trees.Statements.Statement is
480
let loc = super_call.location
481
let arg_exprs = Collections.LIST[Trees.Expressions.Expression]()
482
483
for arg_expr in super_call.args do
484
arg_exprs.add(arg_expr)
485
od
486
487
// super.init(args)
488
let super_expr = Trees.Expressions.SUPER(loc)
489
let init_identifier = Trees.Identifiers.Identifier(loc, "init")
490
let member =
491
Trees.Expressions.MEMBER(
492
loc,
493
super_expr,
494
init_identifier,
495
loc
496
)
497
let call =
498
Trees.Expressions.CALL(
499
loc,
500
member,
501
Trees.Expressions.LIST(loc, arg_exprs)
502
)
503
504
return Trees.Statements.EXPRESSION(loc, call)
505
si
506
507
_make_capture_assignment(capture: Trees.Definitions.PROPERTY, param: Trees.Variables.VARIABLE) -> Trees.Statements.Statement is
508
let loc = capture.location
509
510
// self.<capture-name> = <param-name>;
511
let self_expr = Trees.Expressions.SELF(loc)
512
let member =
513
Trees.Expressions.MEMBER(
514
loc,
515
self_expr,
516
capture.name!.copy(),
517
loc
518
)
519
let left = Trees.Expressions.SIMPLE_LEFT_EXPRESSION(loc, member)
520
let right =
521
Trees.Expressions.IDENTIFIER(
522
loc,
523
param.name!.copy()
524
)
525
526
return Trees.Statements.ASSIGNMENT(loc, left, right)
527
si
528
529
// Expand `..` in the variant's field list against the
530
// enclosing union's primary parameters. The splice marker is
531
// replaced in-place by deep-copies of the primary params, each
532
// marked is_inherited_primary. They stay in variant.fields so
533
// every existing consumer (arity, equality, hash, destructure)
534
// continues to see the variant's full shape. declare_symbols
535
// skips declaring inherited entries as variant-side fields —
536
// the union base owns the storage and variants inherit through
537
// the union → variant base-class relationship. The variant's
538
// synthesised init forwards inherited args to super.init(...);
539
// non-inherited fields get the usual self.<f> = <f>. Validates
540
// exactly one `..` per variant.
541
_expand_variant_splice(variant: Trees.Definitions.VARIANT, primary_params: Trees.Variables.LIST) is
542
if variant.is_poisoned then
543
return
544
fi
545
546
let splice_count = _count_splices(variant.fields)
547
548
if splice_count == 0 then
549
if variant.fields.count == 0 then
550
// No field list — splice the primary parameters as if
551
// the user had written `(..)`.
552
let new_fields = Collections.LIST[Trees.Variables.VARIABLE]()
553
554
for p in primary_params do
555
let inherited = _copy_variable(p)
556
inherited.mark_inherited_primary(_body_property_name(p).name)
557
new_fields.add(inherited)
558
od
559
560
variant.set_fields(Trees.Variables.LIST(variant.fields.location, new_fields))
561
return
562
fi
563
564
_logger.error(
565
variant.location,
566
"variant of a union with a primary constructor header must include .. to splice in the primary parameters"
567
)
568
return
569
fi
570
571
if splice_count > 1 then
572
_logger.error(
573
variant.location,
574
"variant field list contains more than one .."
575
)
576
fi
577
578
let new_fields = Collections.LIST[Trees.Variables.VARIABLE]()
579
let expanded mut = false
580
581
for v in variant.fields do
582
if v.is_splice then
583
if !expanded then
584
for p in primary_params do
585
let inherited = _copy_variable(p)
586
inherited.mark_inherited_primary(_body_property_name(p).name)
587
new_fields.add(inherited)
588
od
589
590
expanded = true
591
fi
592
else
593
new_fields.add(v)
594
fi
595
od
596
597
variant.set_fields(Trees.Variables.LIST(variant.fields.location, new_fields))
598
si
599
600
_reject_stray_splice(variant: Trees.Definitions.VARIANT) is
601
if variant.is_poisoned then
602
return
603
fi
604
605
for v in variant.fields do
606
if v.is_splice /\ !v.is_poisoned then
607
_logger.error(
608
v.location,
609
".. requires the surrounding union to declare a primary constructor header"
610
)
611
v.poison(true)
612
fi
613
od
614
si
615
616
_expand_secondary_init(f: Trees.Definitions.FUNCTION, primary_params: Trees.Variables.LIST) is
617
// Build the new argument list: every existing argument
618
// except the splice marker, with the primary parameters
619
// spliced in at the first splice's position. Any extra
620
// splice markers (already flagged as errors above) are
621
// dropped — expanding more than one would duplicate the
622
// primary parameter names and cascade into redefinition
623
// diagnostics that the user's own error already accounts
624
// for.
625
let old_args = f.arguments.variables
626
let new_args = Collections.LIST[Trees.Variables.VARIABLE]()
627
let expanded mut = false
628
629
for v in old_args do
630
if v.is_splice then
631
if !expanded then
632
for p in primary_params do
633
new_args.add(_copy_variable(p))
634
od
635
636
expanded = true
637
fi
638
else
639
new_args.add(v)
640
fi
641
od
642
643
let expanded_args =
644
Trees.Variables.LIST(
645
f.arguments.location,
646
new_args
647
)
648
649
f.arguments = expanded_args
650
651
// Prepend `self.init(<primary args>);` to the body.
652
if f.body? /\ isa Trees.Bodies.BLOCK(f.body) then
653
let block = cast Trees.Bodies.BLOCK(f.body)
654
let chain = _make_self_init_chain(f.location, primary_params)
655
656
let new_statements = Collections.LIST[Trees.Statements.Statement]()
657
new_statements.add(chain)
658
659
for s in block.statements do
660
new_statements.add(s)
661
od
662
663
block.statements.replace_all(new_statements)
664
fi
665
si
666
667
_make_self_init_chain(loc: LOCATION, primary_params: Trees.Variables.LIST) -> Trees.Statements.Statement is
668
let arg_exprs = Collections.LIST[Trees.Expressions.Expression]()
669
670
for p in primary_params do
671
arg_exprs.add(
672
Trees.Expressions.IDENTIFIER(loc, p.name!.copy())
673
)
674
od
675
676
let self_expr = Trees.Expressions.SELF(loc)
677
let init_identifier = Trees.Identifiers.Identifier(loc, "init")
678
let member =
679
Trees.Expressions.MEMBER(
680
loc,
681
self_expr,
682
init_identifier,
683
loc
684
)
685
let call =
686
Trees.Expressions.CALL(
687
loc,
688
member,
689
Trees.Expressions.LIST(loc, arg_exprs)
690
)
691
692
return Trees.Statements.EXPRESSION(loc, call)
693
si
694
695
_copy_variable(v: Trees.Variables.VARIABLE) -> Trees.Variables.VARIABLE is
696
let result =
697
Trees.Variables.VARIABLE(
698
v.location,
699
v.name!.copy(),
700
v.type_expression.copy(),
701
false,
702
true,
703
null
704
)
705
706
result.mark_argument()
707
708
return result
709
si
710
711
_copy_variable_list(list: Trees.Variables.LIST) -> Trees.Variables.LIST is
712
let copied = Collections.LIST[Trees.Variables.VARIABLE]()
713
714
for v in list do
715
copied.add(_copy_variable(v))
716
od
717
718
return Trees.Variables.LIST(list.location, copied)
719
si
720
si
721
si