Skip to content
← Back

src/syntax/process/rewrite_primary_constructors.ghul

1
namespace Syntax.Process is
2
use Source;
3
use Logging;
4
use Trees;
5
6
// Walks a `super(...)` argument expression tree and collects the
7
// names of primary-ctor parameters referenced as bare variable
8
// expressions (e.g. `super(x, f(x))` — `x` referenced). Member
9
// names in `a.x` and type-arg names in `LIST[T]` are deliberately
10
// NOT collected: only `visit(Expressions.IDENTIFIER)` fires, so the
11
// bare `Identifier` nodes for member names and type arguments never
12
// reach this visitor.
13
//
14
// Used to gate auto-field synthesis: a primary param consumed by
15
// `super(...)` does not auto-generate a same-named field.
16
class SUPER_PARAM_REFERENCE_COLLECTOR(_primary_param_names: Collections.SET[string]): Syntax.Visitor is
17
referenced_names: Collections.SET[string] public;
18
19
super();
20
21
init(..) is
22
referenced_names = Collections.SET[string]();
23
si
24
25
visit(identifier: Trees.Expressions.IDENTIFIER) is
26
let name = identifier.identifier.name;
27
28
if _primary_param_names.contains(name) then
29
referenced_names.add(name);
30
fi
31
si
32
si
33
34
// Rewrites `class FOO(p1: T1, p2: T2) is ... si` into the
35
// classic-form equivalent. Runs at the head of
36
// `rewrite-syntax-trees` so the synthesised init (and any
37
// captured-public fields) flow through `add_accessors_for_properties`
38
// exactly like a hand-written init / hand-written public field would.
39
//
40
// Surface this visitor consumes:
41
// - Classy.primary_params: parameters parsed from the `(...)` header.
42
// Modifier suffixes on each param (`public` / `field` / `init`)
43
// are read here and ride through to the auto-generated body decl
44
// (or, for `init`, suppress it).
45
// - Body-level `super(expr, expr);` declarations (Definitions.SUPER_CALL).
46
// Each arg can be any expression whose only free names are primary-
47
// ctor params; literals and module/type-level references are also
48
// in scope. Walking the expression trees identifies which primary
49
// params are consumed by `super(...)` so they aren't also auto-
50
// field-generated.
51
// - Body-level capture shorthand: a field declaration with INFER
52
// (no explicit) type expression and no read/assign body. Matches a
53
// primary parameter named `<field-name>` or `_<field-name>`. Typed
54
// body decls match the same way — body decl wins, no auto-gen.
55
// - Primary parameters that match nothing in the body and are not in
56
// `super(...)` and lack the `init` modifier: an auto-field is
57
// synthesised before the body iteration so downstream phases see
58
// it as if the user had written it by hand.
59
// - Secondary-init formal arg lists containing a VARIABLE flagged
60
// is_splice (the `..` marker). Expanded to the primary parameters
61
// in place.
62
//
63
// After this visitor runs on a Classy, `primary_params` is null and
64
// every SUPER_CALL / capture-field / splice in the body has been
65
// consumed; downstream phases see a node indistinguishable from a
66
// class written in classic form.
67
class REWRITE_PRIMARY_CONSTRUCTORS: Visitor is
68
_logger: Logger;
69
70
init(logger: Logger) is
71
super.init();
72
73
_logger = logger;
74
si
75
76
apply(node: Trees.Node) is
77
node.walk(self);
78
si
79
80
pre(`class: Trees.Definitions.CLASS) -> bool is
81
_lower(`class);
82
return false;
83
si
84
85
pre(`struct: Trees.Definitions.STRUCT) -> bool is
86
_lower(`struct);
87
return false;
88
si
89
90
// A union with a primary-constructor header lowers the same
91
// way as a class: the primary params become auto-fields on the
92
// union base class and feed the synthesised init. A variant
93
// with additional fields must include `..` exactly once to
94
// splice the primary params in; a variant with no field list
95
// at all (just `NAME;`) has the splice implied. The splice
96
// expansion replaces the marker with deep-copies of the
97
// primary params (marked is_inherited_primary so the variant's
98
// init synthesis can forward them to super.init(...) rather
99
// than reassigning them).
100
//
101
// A union without a primary header rejects stray `..` in any
102
// variant — the splice has no source to expand against.
103
pre(`union: Trees.Definitions.UNION) -> bool is
104
let primary_params = `union.primary_params;
105
106
_lower(`union);
107
108
if primary_params? then
109
for member in `union.body do
110
if isa Trees.Definitions.VARIANT(member) then
111
_expand_variant_splice(cast Trees.Definitions.VARIANT(member), primary_params);
112
fi
113
od
114
else
115
for member in `union.body do
116
if isa Trees.Definitions.VARIANT(member) then
117
_reject_stray_splice(cast Trees.Definitions.VARIANT(member));
118
fi
119
od
120
fi
121
122
return false;
123
si
124
125
// The parser allows `..` in any `init(...)` formal-arg list,
126
// because that's what's syntactically valid; the semantic
127
// requirement (init must belong to a primary-constructor
128
// class) can't be enforced at parse time. By the time the
129
// walk reaches a VARIABLE in a primary class, `_expand_secondary_init`
130
// has replaced every splice with the primary parameters, so
131
// any surviving splice came from a non-primary class.
132
pre(variable: Trees.Variables.VARIABLE) -> bool is
133
if variable.is_splice /\ !variable.is_poisoned then
134
_logger.error(
135
variable.location,
136
".. requires a primary constructor header on the surrounding class"
137
);
138
fi
139
return false;
140
si
141
142
_lower(classy: Trees.Definitions.Classy) is
143
let primary_params: Trees.Variables.LIST? mut = classy.primary_params;
144
145
if !primary_params? then
146
// No primary header — a `super(...)` written in the
147
// class body has nowhere to forward arguments to. Flag
148
// each one and drop it from the body so downstream
149
// visitors don't trip over the unconsumed node.
150
_strip_orphan_super_calls(classy);
151
return;
152
fi
153
154
// The variable parser already rejected `..` in the class
155
// header (`in_init_arguments` is false there). What can
156
// still reach here is a destructuring left
157
// (e.g. `(a, b): PAIR`), which has no simple name to use
158
// for capture matching or as the synthesised init's
159
// formal-argument name. Flag and filter so the synthesised
160
// init doesn't inherit a broken argument.
161
for v in primary_params do
162
if !v.name? then
163
_logger.error(
164
v.location,
165
"primary constructor parameter must have a simple name"
166
);
167
fi
168
od
169
170
primary_params = _filter_unusable_primary_params(primary_params);
171
172
let class_location = classy.location;
173
174
// Validate primary-param modifier combinations once up-front;
175
// diagnostics target the param's location so the user sees the
176
// offending param, not the body downstream.
177
for p in primary_params do
178
_validate_primary_param_modifiers(p);
179
od
180
181
// Index field-shaped properties by name; per primary
182
// parameter prefer the `_foo` match then fall back to bare
183
// `foo`; first match wins.
184
let field_props_by_name = Collections.MAP[string, Trees.Definitions.PROPERTY]();
185
186
for d in classy.body do
187
if isa Trees.Definitions.PROPERTY(d) then
188
let p = cast Trees.Definitions.PROPERTY?(d)!;
189
190
if
191
!p.read_body? /\
192
!p.assign_body? /\
193
p.name? /\
194
!field_props_by_name.contains_key(p.name.name)
195
then
196
field_props_by_name[p.name.name] = p;
197
fi
198
fi
199
od
200
201
let chosen_captures = Collections.SET[Trees.Definitions.PROPERTY]();
202
let param_for_capture = Collections.MAP[Trees.Definitions.PROPERTY, Trees.Variables.VARIABLE]();
203
let primary_params_by_name = Collections.MAP[string, Trees.Variables.VARIABLE]();
204
let primary_param_names = Collections.SET[string]();
205
let already_captured_param_names = Collections.SET[string]();
206
207
for p in primary_params do
208
let name = p.name!.name;
209
210
primary_params_by_name[name] = p;
211
primary_param_names.add(name);
212
213
let chosen = _find_chosen_capture(name, field_props_by_name, chosen_captures);
214
215
if chosen? then
216
chosen_captures.add(chosen);
217
param_for_capture[chosen] = p;
218
already_captured_param_names.add(name);
219
fi
220
od
221
222
// First sweep over the body — locate `super(...)` so its
223
// argument expressions can be walked for primary-param
224
// references (used below to suppress auto-field synthesis
225
// for params consumed by super). The full body iteration
226
// happens later; this pre-scan never mutates classy.body.
227
let super_call_for_refs: Trees.Definitions.SUPER_CALL? mut = null;
228
for d in classy.body do
229
if isa Trees.Definitions.SUPER_CALL(d) /\ !super_call_for_refs? then
230
super_call_for_refs = cast Trees.Definitions.SUPER_CALL(d);
231
fi
232
od
233
234
let super_referenced_param_names = Collections.SET[string]();
235
if super_call_for_refs? then
236
let collector = SUPER_PARAM_REFERENCE_COLLECTOR(primary_param_names);
237
for arg_expr in super_call_for_refs.args do
238
arg_expr.walk(collector);
239
od
240
for name in collector.referenced_names do
241
super_referenced_param_names.add(name);
242
od
243
fi
244
245
// Auto-field synthesis: a primary param that isn't matched by
246
// an existing body decl, isn't referenced by `super(...)`, and
247
// doesn't carry the `init` modifier auto-generates a body
248
// field/property. Inserted at the head of the body in primary-
249
// header order so member ordering reads naturally. Downstream
250
// (`add_accessors_for_properties`, `declare_symbols`) treats
251
// the synthesised node exactly like a hand-written body decl.
252
let auto_gen_props = Collections.LIST[Trees.Definitions.PROPERTY]();
253
for p in primary_params do
254
let name = p.name!.name;
255
256
if already_captured_param_names.contains(name) then
257
continue;
258
fi
259
if super_referenced_param_names.contains(name) then
260
continue;
261
fi
262
if let p.modifiers? /\ modifiers.is_init then
263
continue;
264
fi
265
let synthesised = _synthesise_auto_property(p);
266
auto_gen_props.add(synthesised);
267
chosen_captures.add(synthesised);
268
param_for_capture[synthesised] = p;
269
already_captured_param_names.add(name);
270
od
271
272
let super_call: Trees.Definitions.SUPER_CALL? mut = null;
273
let captures = Collections.LIST[Trees.Definitions.PROPERTY]();
274
let user_primary_init: Trees.Definitions.FUNCTION? mut = null;
275
let secondary_inits = Collections.LIST[Trees.Definitions.FUNCTION]();
276
let kept = Collections.LIST[Trees.Definitions.Definition]();
277
278
// Auto-gen props ride at the head of the body so they precede
279
// user-written members, matching the order of the primary
280
// header.
281
for synthesised in auto_gen_props do
282
captures.add(synthesised);
283
kept.add(synthesised);
284
od
285
286
for d in classy.body do
287
if isa Trees.Definitions.SUPER_CALL(d) then
288
let sc = cast Trees.Definitions.SUPER_CALL(d);
289
290
if super_call? then
291
_logger.error(sc.location, "duplicate super(...) declaration");
292
else
293
super_call = sc;
294
fi
295
elif _has_init_modifier(d) then
296
_logger.error(
297
d.location,
298
"init modifier is only valid on a primary constructor parameter"
299
);
300
kept.add(d);
301
elif isa Trees.Definitions.PROPERTY(d) /\ chosen_captures.contains(cast Trees.Definitions.PROPERTY(d)) then
302
let capture = cast Trees.Definitions.PROPERTY(d);
303
captures.add(capture);
304
kept.add(d);
305
elif _is_unmatched_capture_shorthand(d) then
306
// Field-shaped property with INFER type but no
307
// matching primary parameter (or the matching one
308
// was already captured by `_foo`, leaving the bare
309
// `foo;` shorthand stranded with no type to fall
310
// back to). Either way the field can't reach
311
// downstream phases — flag it and drop it.
312
let p = cast Trees.Definitions.PROPERTY?(d)!;
313
let stripped = _strip_underscore(p.name!.name);
314
315
if already_captured_param_names.contains(stripped) then
316
_logger.error(
317
p.location,
318
"primary parameter {stripped} is already captured by _{stripped}"
319
);
320
else
321
_logger.error(
322
p.location,
323
"no primary parameter named {stripped} to capture"
324
);
325
fi
326
elif _is_init_function(d) then
327
let f = cast Trees.Definitions.FUNCTION?(d)!;
328
let has_splice = _arg_list_has_splice(f.arguments);
329
330
if has_splice then
331
let splice_count = _count_splices(f.arguments);
332
333
if splice_count > 1 then
334
_logger.error(f.location, "init parameter list contains more than one ..");
335
fi
336
337
if _is_primary_init(f) then
338
if user_primary_init? then
339
_logger.error(f.location, "duplicate primary constructor init body");
340
else
341
user_primary_init = f;
342
fi
343
else
344
secondary_inits.add(f);
345
fi
346
else
347
kept.add(d);
348
fi
349
else
350
kept.add(d);
351
fi
352
od
353
354
// Fill in INFER type expressions from the chosen primary
355
// parameter. Captures with an explicit type (e.g.
356
// `x: int public;` to widen visibility) keep their type.
357
for capture in captures do
358
let matching = param_for_capture[capture];
359
360
if capture.type_expression.is_inferred then
361
capture.set_type_expression(matching.type_expression.copy());
362
fi
363
od
364
365
// Synthesise the per-capture assignment statements.
366
let auto_statements = Collections.LIST[Trees.Statements.Statement]();
367
368
if super_call? then
369
auto_statements.add(_make_super_init_call(super_call));
370
fi
371
372
for capture in captures do
373
auto_statements.add(_make_capture_assignment(capture, param_for_capture[capture]));
374
od
375
376
// If the user wrote `init(..) is body si`, splice their
377
// statements in after the auto-generated assignments.
378
if user_primary_init? /\ user_primary_init.body? /\ isa Trees.Bodies.BLOCK(user_primary_init.body) then
379
let user_block = cast Trees.Bodies.BLOCK(user_primary_init.body);
380
381
for s in user_block.statements do
382
auto_statements.add(s);
383
od
384
fi
385
386
// The BLOCK's own location is internal: the body has no
387
// source text the user wrote, and giving it the class span
388
// would make the incremental body re-walk treat every
389
// interface symbol on the class header (the class itself,
390
// its auto-properties) as `inside` this body and drop them
391
// from the symbol-definition map after an EDIT.
392
let primary_init_body =
393
Trees.Bodies.BLOCK(
394
LOCATION.internal,
395
Trees.Statements.LIST(LOCATION.internal, auto_statements)
396
);
397
398
// Deep-copy the primary parameters into a fresh argument
399
// list for the synthesised init — keeps the original list
400
// available for the secondary-init splice expansion below
401
// without aliasing the same VARIABLE instances across two
402
// FUNCTION nodes.
403
let primary_init_args = _copy_variable_list(primary_params);
404
405
// Anchor the synthesised init's overall span to the class
406
// header (class name through primary parameter list). A
407
// user-written `init(...)` at the same signature picks up
408
// the duplicate-method diagnostic against this range; the
409
// synthesised init also surfaces in the VSCE outline at
410
// the class-header position.
411
//
412
// The name identifier is anchored separately: when the
413
// user wrote `init(..) is ... si`, point it at the user's
414
// own `init` source location so hover, goto-definition,
415
// and the semantic-tokens classifier resolve to this
416
// symbol at the user-typed keyword. Without that, the
417
// user's `init` has no recorded symbol use and falls
418
// back to TextMate colouring with no hover. For the
419
// auto-generated case (no user `init(..)`) the name stays
420
// at the class header.
421
let header_location = classy.name.location :: primary_params.location;
422
423
let primary_init_name =
424
if user_primary_init? /\ user_primary_init.name? then
425
user_primary_init.name.copy();
426
else
427
Trees.Identifiers.Identifier(header_location, "init");
428
fi;
429
430
let primary_init =
431
Trees.Definitions.FUNCTION(
432
header_location,
433
primary_init_name,
434
Trees.TypeExpressions.LIST(LOCATION.internal, Collections.LIST[Trees.TypeExpressions.TypeExpression](0)),
435
primary_init_args,
436
Trees.TypeExpressions.INFER(class_location),
437
Trees.Modifiers.LIST(class_location, null, null),
438
primary_init_body
439
);
440
441
primary_init.is_primary_constructor = true;
442
443
kept.add(primary_init);
444
445
// Re-attach each secondary init with its splice expanded
446
// and an implicit chain to the primary init prepended.
447
for f in secondary_inits do
448
_expand_secondary_init(f, primary_params);
449
kept.add(f);
450
od
451
452
// Auto-deconstruct synthesis. When the user has not written a
453
// `deconstruct(...)` method and has not exposed any
454
// conventionally-named positional members (`0`, `1`, ...),
455
// synthesise a `deconstruct` exposing every public-readable
456
// capture in primary-header order. The resolver's precedence
457
// (tuple > deconstruct > positional > by-name) means a
458
// user-supplied `0`/`1`/... continues to be the explicit opt-in
459
// to positional access — co-existing with the synthesised
460
// method would make it unreachable.
461
_maybe_synthesise_deconstruct(
462
classy,
463
class_location,
464
primary_params,
465
param_for_capture,
466
already_captured_param_names,
467
kept
468
);
469
470
// Replace the body's contents with the curated list.
471
classy.body.clear_definitions();
472
473
for d in kept do
474
classy.body.add(d);
475
od
476
477
classy.set_primary_params(null);
478
si
479
480
_find_chosen_capture(
481
param_name: string,
482
field_props_by_name: Collections.MAP[string, Trees.Definitions.PROPERTY],
483
already_chosen: Collections.SET[Trees.Definitions.PROPERTY]
484
) -> Trees.Definitions.PROPERTY? is
485
let underscore_name = "_{param_name}";
486
487
if field_props_by_name.contains_key(underscore_name) then
488
let candidate = field_props_by_name[underscore_name];
489
490
if !already_chosen.contains(candidate) then
491
return candidate;
492
fi
493
fi
494
495
if field_props_by_name.contains_key(param_name) then
496
let candidate = field_props_by_name[param_name];
497
498
if !already_chosen.contains(candidate) then
499
return candidate;
500
fi
501
fi
502
503
return null;
504
si
505
506
_filter_unusable_primary_params(args: Trees.Variables.LIST) -> Trees.Variables.LIST is
507
let kept = Collections.LIST[Trees.Variables.VARIABLE]();
508
509
for v in args do
510
if v.name? then
511
kept.add(v);
512
fi
513
od
514
515
if kept.count == args.count then
516
return args;
517
fi
518
519
return Trees.Variables.LIST(args.location, kept);
520
si
521
522
_strip_orphan_super_calls(classy: Trees.Definitions.Classy) is
523
let kept = Collections.LIST[Trees.Definitions.Definition]();
524
let any_dropped mut = false;
525
526
for d in classy.body do
527
if isa Trees.Definitions.SUPER_CALL(d) then
528
_logger.error(
529
d.location,
530
"super(...) declaration requires a primary constructor header"
531
);
532
any_dropped = true;
533
else
534
if _has_init_modifier(d) then
535
_logger.error(
536
d.location,
537
"init modifier is only valid on a primary constructor parameter"
538
);
539
fi
540
kept.add(d);
541
fi
542
od
543
544
if any_dropped then
545
classy.body.clear_definitions();
546
547
for d in kept do
548
classy.body.add(d);
549
od
550
fi
551
si
552
553
_is_unmatched_capture_shorthand(d: Trees.Definitions.Definition) -> bool is
554
if !isa Trees.Definitions.PROPERTY(d) then
555
return false;
556
fi
557
558
let p = cast Trees.Definitions.PROPERTY(d);
559
560
if p.read_body? \/ p.assign_body? then
561
return false;
562
fi
563
564
return p.name? /\ p.type_expression.is_inferred;
565
si
566
567
_is_init_function(d: Trees.Definitions.Definition) -> bool is
568
if !isa Trees.Definitions.FUNCTION(d) then
569
return false;
570
fi
571
572
let f = cast Trees.Definitions.FUNCTION(d);
573
574
return f.name? /\ f.name.name =~ "init";
575
si
576
577
_is_primary_init(f: Trees.Definitions.FUNCTION) -> bool is
578
// init(..) — exactly one parameter and it's the splice marker.
579
let args = f.arguments.variables;
580
581
return args.count == 1 /\ args[0].is_splice;
582
si
583
584
_arg_list_has_splice(args: Trees.Variables.LIST?) -> bool is
585
if !args? then
586
return false;
587
fi
588
589
for v in args do
590
if v.is_splice then
591
return true;
592
fi
593
od
594
595
return false;
596
si
597
598
_count_splices(args: Trees.Variables.LIST?) -> int is
599
if !args? then
600
return 0;
601
fi
602
603
let n mut = 0;
604
605
for v in args do
606
if v.is_splice then
607
n = n + 1;
608
fi
609
od
610
611
return n;
612
si
613
614
_strip_underscore(name: string) -> string is
615
if name.length > 0 /\ name.get_chars(0) == '_' then
616
return name.substring(1);
617
fi
618
619
return name;
620
si
621
622
// Diagnoses incoherent combinations on a primary-ctor parameter:
623
// - `init` combined with any visibility modifier or with another
624
// storage modifier means "no field, but also make the (non-
625
// existent) field public / a plain field" — incoherent.
626
// - `init` combined with a `_`-prefixed name means "no field, but
627
// the name implies a private field" — same incoherence.
628
// The rewriter only flags; it doesn't strip, so the user sees
629
// every diagnostic in one pass.
630
_validate_primary_param_modifiers(p: Trees.Variables.VARIABLE) is
631
let modifiers = p.modifiers;
632
633
if !modifiers? then
634
return;
635
fi
636
637
if modifiers.is_init then
638
if let modifiers.access_modifier? then
639
_logger.error(
640
access_modifier.location,
641
"init modifier cannot combine with visibility modifiers"
642
);
643
fi
644
if let
645
modifiers.storage_class? /\
646
!storage_class.is_init
647
then
648
_logger.error(
649
storage_class.location,
650
"init modifier cannot combine with other storage modifiers"
651
);
652
fi
653
if let
654
p.name? /\
655
name.name.length > 0 /\
656
name.name.get_chars(0) == '_'
657
then
658
_logger.error(
659
p.location,
660
"init modifier cannot combine with a _-prefixed parameter name"
661
);
662
fi
663
fi
664
si
665
666
// Body decls with the `init` modifier are diagnosed at the body-
667
// iteration step. This helper recognises the (FUNCTION / PROPERTY)
668
// shapes that carry modifier lists.
669
_has_init_modifier(d: Trees.Definitions.Definition) -> bool is
670
if isa Trees.Definitions.PROPERTY(d) then
671
let p = cast Trees.Definitions.PROPERTY(d);
672
return p.modifiers.is_init;
673
fi
674
if isa Trees.Definitions.FUNCTION(d) then
675
let f = cast Trees.Definitions.FUNCTION(d);
676
return f.modifiers.is_init;
677
fi
678
return false;
679
si
680
681
// Synthesise a body-level field/property declaration mirroring the
682
// primary-ctor parameter, ready to flow through
683
// `add_accessors_for_properties` and `declare_symbols` as if the
684
// user had written it by hand. Strips the `init` modifier from
685
// the synthesised modifier list as a safety net — even though the
686
// caller has already filtered INIT params, leaving INIT on the
687
// body decl would re-trigger the "init only on primary param"
688
// diagnostic.
689
_synthesise_auto_property(p: Trees.Variables.VARIABLE) -> Trees.Definitions.PROPERTY =>
690
let loc = p.location in
691
let modifiers = _make_body_modifiers(p) in
692
Trees.Definitions.PROPERTY(
693
loc,
694
p.type_expression.copy(),
695
_body_property_name(p),
696
modifiers,
697
null,
698
null,
699
null
700
);
701
702
// `private` names the member the way the naming convention does:
703
// `v: int private` captures into `_v`, leaving the parameter
704
// itself as `v`. The underscore is what the later passes read for
705
// visibility, so `_make_body_modifiers` drops the modifier rather
706
// than carrying both spellings of the same fact.
707
_body_property_name(p: Trees.Variables.VARIABLE) -> Trees.Identifiers.Identifier is
708
let name = p.name!;
709
710
if _is_private_param(p) /\ !name.name.starts_with('_') then
711
return Trees.Identifiers.Identifier(name.location, "_{name.name}");
712
fi
713
714
return name.copy();
715
si
716
717
_is_private_param(p: Trees.Variables.VARIABLE) -> bool is
718
if let p.modifiers?, modifiers.access_modifier? /\ access_modifier.is_private then
719
return true;
720
fi
721
722
return false;
723
si
724
725
_make_body_modifiers(p: Trees.Variables.VARIABLE) -> Trees.Modifiers.LIST is
726
let loc = p.location;
727
let modifiers = p.modifiers;
728
729
if !modifiers? then
730
return Trees.Modifiers.LIST(loc, null, null);
731
fi
732
733
let access: Trees.Modifiers.AccessModifier? mut = null;
734
let storage: Trees.Modifiers.StorageClass? mut = null;
735
736
if let modifiers.access_modifier? /\ !access_modifier.is_private then
737
access = access_modifier.copy();
738
fi
739
740
// Drop INIT — it has no meaning on the body decl. Other
741
// storage classes (FIELD, etc.) flow through.
742
if let modifiers.storage_class? /\ !storage_class.is_init then
743
storage = storage_class.copy();
744
fi
745
746
return Trees.Modifiers.LIST(loc, access, storage);
747
si
748
749
// See `DESTRUCTURE_RESOLVER` for the precedence the synthesised
750
// deconstruct slots into. Guard preconditions:
751
// - User wrote no `deconstruct` of any arity in this class body.
752
// - User exposed no backtick-numeric property (`0`, `1`, ...).
753
// - At least one public-readable capture exists to expose.
754
// The synthesised method is `public`, instance, with one `T ref`
755
// parameter per included capture (named after the capture so the
756
// body reads naturally; the destructure call site only ever
757
// matches by arity + ref-ness).
758
_maybe_synthesise_deconstruct(
759
classy: Trees.Definitions.Classy,
760
class_location: LOCATION,
761
primary_params: Trees.Variables.LIST,
762
param_for_capture: Collections.MAP[Trees.Definitions.PROPERTY, Trees.Variables.VARIABLE],
763
already_captured_param_names: Collections.SET[string],
764
kept: Collections.MutableList[Trees.Definitions.Definition]
765
) is
766
for d in classy.body do
767
if isa Trees.Definitions.FUNCTION(d) then
768
let f = cast Trees.Definitions.FUNCTION?(d)!;
769
if f.name? /\ f.name.name =~ "deconstruct" then
770
return;
771
fi
772
elif isa Trees.Definitions.PROPERTY(d) then
773
let p = cast Trees.Definitions.PROPERTY?(d)!;
774
if p.name? /\ Semantic.Symbols.Symbol.is_positional_member_name(p.name.name) then
775
return;
776
fi
777
fi
778
od
779
780
// Reverse-lookup table: primary-ctor parameter name -> its
781
// capture property. Built once so the capture-collection loop
782
// below stays O(captures).
783
let capture_for_param_name = Collections.MAP[string, Trees.Definitions.PROPERTY]();
784
for kvp in param_for_capture do
785
let cap = kvp.key;
786
let param = kvp.value;
787
if let param.name? then
788
capture_for_param_name[name.name] = cap;
789
fi
790
od
791
792
let deconstruct_captures = Collections.LIST[Trees.Definitions.PROPERTY]();
793
for p in primary_params do
794
let name = p.name;
795
if !name? then
796
continue;
797
fi
798
if !already_captured_param_names.contains(name.name) then
799
continue;
800
fi
801
if !capture_for_param_name.contains_key(name.name) then
802
continue;
803
fi
804
let capture = capture_for_param_name[name.name];
805
if !_is_public_readable_capture(capture) then
806
continue;
807
fi
808
deconstruct_captures.add(capture);
809
od
810
811
if deconstruct_captures.count == 0 then
812
return;
813
fi
814
815
let deconstruct = _synthesise_deconstruct(class_location, deconstruct_captures);
816
817
// Wrap in an @IL.name("Deconstruct") pragma so the method
818
// satisfies the standard .NET `Deconstruct` contract used by
819
// C# positional patterns and other cross-language consumers.
820
kept.add(_wrap_in_il_name_pragma(class_location, "Deconstruct", deconstruct));
821
si
822
823
_is_public_readable_capture(p: Trees.Definitions.PROPERTY) -> bool is
824
// ghūl carries read-visibility two ways: an explicit
825
// access modifier, or a leading `_` on the name (the
826
// convention: "_x" -> protected for reading). Both must
827
// be checked — the synthesised deconstruct is `public`,
828
// and surfacing a protected member through it would
829
// bypass the encapsulation the user signalled.
830
if let p.modifiers?, modifiers.access_modifier? /\ access_modifier.is_private then
831
return false;
832
fi
833
if p.name? /\ p.name.name.length > 0 /\ p.name.name.get_chars(0) == '_' then
834
return false;
835
fi
836
return true;
837
si
838
839
_synthesise_deconstruct(
840
class_location: LOCATION,
841
captures: Collections.List[Trees.Definitions.PROPERTY]
842
) -> Trees.Definitions.FUNCTION is
843
let arg_vars = Collections.LIST[Trees.Variables.VARIABLE]();
844
let body_stmts = Collections.LIST[Trees.Statements.Statement]();
845
846
for capture in captures do
847
let cap_loc = capture.location;
848
let arg_type =
849
Trees.TypeExpressions.REFERENCE(
850
cap_loc,
851
capture.type_expression.copy()
852
);
853
let arg =
854
Trees.Variables.VARIABLE(
855
cap_loc,
856
capture.name!.copy(),
857
arg_type,
858
false,
859
true,
860
null
861
);
862
arg.mark_argument();
863
arg_vars.add(arg);
864
865
body_stmts.add(_make_deconstruct_assignment(capture));
866
od
867
868
let arg_list = Trees.Variables.LIST(class_location, arg_vars);
869
let body =
870
Trees.Bodies.BLOCK(
871
LOCATION.internal,
872
Trees.Statements.LIST(LOCATION.internal, body_stmts)
873
);
874
let modifiers =
875
Trees.Modifiers.LIST(
876
class_location,
877
Trees.Modifiers.PUBLIC(class_location),
878
null
879
);
880
881
return
882
Trees.Definitions.FUNCTION(
883
class_location,
884
Trees.Identifiers.Identifier(LOCATION.internal, "deconstruct"),
885
Trees.TypeExpressions.LIST(LOCATION.internal, Collections.LIST[Trees.TypeExpressions.TypeExpression](0)),
886
arg_list,
887
Trees.TypeExpressions.INFER(class_location),
888
modifiers,
889
body
890
);
891
si
892
893
_make_deconstruct_assignment(
894
capture: Trees.Definitions.PROPERTY
895
) -> Trees.Statements.Statement is
896
let loc = capture.location;
897
898
// <capture-name>! = self.<capture-name>;
899
//
900
// Postfix `!` on a `T ref` LHS lowers to a `stobj <T>` —
901
// the write-through-ref form. Without it,
902
// `<capture-name> = self.<...>` would error with
903
// "Ghul.T is not assignable to Ghul.T ref".
904
let arg_ref =
905
Trees.Expressions.IDENTIFIER(loc, capture.name!.copy());
906
let unwrap = Trees.Expressions.UNWRAP(loc, arg_ref);
907
let left = Trees.Expressions.SIMPLE_LEFT_EXPRESSION(loc, unwrap);
908
909
let self_expr = Trees.Expressions.SELF(loc);
910
let right =
911
Trees.Expressions.MEMBER(
912
loc,
913
self_expr,
914
capture.name!.copy(),
915
loc
916
);
917
918
return Trees.Statements.ASSIGNMENT(loc, left, right);
919
si
920
921
_wrap_in_il_name_pragma(
922
loc: LOCATION,
923
il_name: string,
924
definition: Trees.Definitions.Definition
925
) -> Trees.Definitions.PRAGMA is
926
let name_literal = Trees.Expressions.Literals.STRING(loc, il_name);
927
let args = Collections.LIST[Trees.Expressions.Expression]();
928
args.add(name_literal);
929
let pragma =
930
Trees.Pragmas.PRAGMA(
931
loc,
932
Trees.Identifiers.Identifier(loc, "IL.name"),
933
Trees.Expressions.LIST(loc, args),
934
null
935
);
936
return Trees.Definitions.PRAGMA(loc, pragma, definition);
937
si
938
939
_make_super_init_call(
940
super_call: Trees.Definitions.SUPER_CALL
941
) -> Trees.Statements.Statement is
942
let loc = super_call.location;
943
let arg_exprs = Collections.LIST[Trees.Expressions.Expression]();
944
945
for arg_expr in super_call.args do
946
arg_exprs.add(arg_expr);
947
od
948
949
// super.init(args)
950
let super_expr = Trees.Expressions.SUPER(loc);
951
let init_identifier = Trees.Identifiers.Identifier(loc, "init");
952
let member =
953
Trees.Expressions.MEMBER(
954
loc,
955
super_expr,
956
init_identifier,
957
loc
958
);
959
let call =
960
Trees.Expressions.CALL(
961
loc,
962
member,
963
Trees.Expressions.LIST(loc, arg_exprs)
964
);
965
966
return Trees.Statements.EXPRESSION(loc, call);
967
si
968
969
_make_capture_assignment(capture: Trees.Definitions.PROPERTY, param: Trees.Variables.VARIABLE) -> Trees.Statements.Statement is
970
let loc = capture.location;
971
972
// self.<capture-name> = <param-name>;
973
let self_expr = Trees.Expressions.SELF(loc);
974
let member =
975
Trees.Expressions.MEMBER(
976
loc,
977
self_expr,
978
capture.name!.copy(),
979
loc
980
);
981
let left = Trees.Expressions.SIMPLE_LEFT_EXPRESSION(loc, member);
982
let right =
983
Trees.Expressions.IDENTIFIER(
984
loc,
985
param.name!.copy()
986
);
987
988
return Trees.Statements.ASSIGNMENT(loc, left, right);
989
si
990
991
// Expand `..` in the variant's field list against the
992
// enclosing union's primary parameters. The splice marker is
993
// replaced in-place by deep-copies of the primary params, each
994
// marked is_inherited_primary. They stay in variant.fields so
995
// every existing consumer (arity, equality, hash, destructure)
996
// continues to see the variant's full shape. declare_symbols
997
// skips declaring inherited entries as variant-side fields —
998
// the union base owns the storage and variants inherit through
999
// the union → variant base-class relationship. The variant's
1000
// synthesised init forwards inherited args to super.init(...);
1001
// non-inherited fields get the usual self.<f> = <f>. Validates
1002
// exactly one `..` per variant.
1003
_expand_variant_splice(variant: Trees.Definitions.VARIANT, primary_params: Trees.Variables.LIST) is
1004
if variant.is_poisoned then
1005
return;
1006
fi
1007
1008
let splice_count = _count_splices(variant.fields);
1009
1010
if splice_count == 0 then
1011
if variant.fields.count == 0 then
1012
// No field list — splice the primary parameters as if
1013
// the user had written `(..)`.
1014
let new_fields = Collections.LIST[Trees.Variables.VARIABLE]();
1015
1016
for p in primary_params do
1017
let inherited = _copy_variable(p);
1018
inherited.mark_inherited_primary();
1019
new_fields.add(inherited);
1020
od
1021
1022
variant.set_fields(Trees.Variables.LIST(variant.fields.location, new_fields));
1023
return;
1024
fi
1025
1026
_logger.error(
1027
variant.location,
1028
"variant of a union with a primary constructor header must include .. to splice in the primary parameters"
1029
);
1030
return;
1031
fi
1032
1033
if splice_count > 1 then
1034
_logger.error(
1035
variant.location,
1036
"variant field list contains more than one .."
1037
);
1038
fi
1039
1040
let new_fields = Collections.LIST[Trees.Variables.VARIABLE]();
1041
let expanded mut = false;
1042
1043
for v in variant.fields do
1044
if v.is_splice then
1045
if !expanded then
1046
for p in primary_params do
1047
let inherited = _copy_variable(p);
1048
inherited.mark_inherited_primary();
1049
new_fields.add(inherited);
1050
od
1051
1052
expanded = true;
1053
fi
1054
else
1055
new_fields.add(v);
1056
fi
1057
od
1058
1059
variant.set_fields(Trees.Variables.LIST(variant.fields.location, new_fields));
1060
si
1061
1062
_reject_stray_splice(variant: Trees.Definitions.VARIANT) is
1063
if variant.is_poisoned then
1064
return;
1065
fi
1066
1067
for v in variant.fields do
1068
if v.is_splice /\ !v.is_poisoned then
1069
_logger.error(
1070
v.location,
1071
".. requires the surrounding union to declare a primary constructor header"
1072
);
1073
v.poison(true);
1074
fi
1075
od
1076
si
1077
1078
_expand_secondary_init(f: Trees.Definitions.FUNCTION, primary_params: Trees.Variables.LIST) is
1079
// Build the new argument list: every existing argument
1080
// except the splice marker, with the primary parameters
1081
// spliced in at the first splice's position. Any extra
1082
// splice markers (already flagged as errors above) are
1083
// dropped — expanding more than one would duplicate the
1084
// primary parameter names and cascade into redefinition
1085
// diagnostics that the user's own error already accounts
1086
// for.
1087
let old_args = f.arguments.variables;
1088
let new_args = Collections.LIST[Trees.Variables.VARIABLE]();
1089
let expanded mut = false;
1090
1091
for v in old_args do
1092
if v.is_splice then
1093
if !expanded then
1094
for p in primary_params do
1095
new_args.add(_copy_variable(p));
1096
od
1097
1098
expanded = true;
1099
fi
1100
else
1101
new_args.add(v);
1102
fi
1103
od
1104
1105
let expanded_args =
1106
Trees.Variables.LIST(
1107
f.arguments.location,
1108
new_args
1109
);
1110
1111
f.arguments = expanded_args;
1112
1113
// Prepend `self.init(<primary args>);` to the body.
1114
if f.body? /\ isa Trees.Bodies.BLOCK(f.body) then
1115
let block = cast Trees.Bodies.BLOCK(f.body);
1116
let chain = _make_self_init_chain(f.location, primary_params);
1117
1118
let new_statements = Collections.LIST[Trees.Statements.Statement]();
1119
new_statements.add(chain);
1120
1121
for s in block.statements do
1122
new_statements.add(s);
1123
od
1124
1125
block.statements.replace_all(new_statements);
1126
fi
1127
si
1128
1129
_make_self_init_chain(loc: LOCATION, primary_params: Trees.Variables.LIST) -> Trees.Statements.Statement is
1130
let arg_exprs = Collections.LIST[Trees.Expressions.Expression]();
1131
1132
for p in primary_params do
1133
arg_exprs.add(
1134
Trees.Expressions.IDENTIFIER(loc, p.name!.copy())
1135
);
1136
od
1137
1138
let self_expr = Trees.Expressions.SELF(loc);
1139
let init_identifier = Trees.Identifiers.Identifier(loc, "init");
1140
let member =
1141
Trees.Expressions.MEMBER(
1142
loc,
1143
self_expr,
1144
init_identifier,
1145
loc
1146
);
1147
let call =
1148
Trees.Expressions.CALL(
1149
loc,
1150
member,
1151
Trees.Expressions.LIST(loc, arg_exprs)
1152
);
1153
1154
return Trees.Statements.EXPRESSION(loc, call);
1155
si
1156
1157
_copy_variable(v: Trees.Variables.VARIABLE) -> Trees.Variables.VARIABLE is
1158
let result =
1159
Trees.Variables.VARIABLE(
1160
v.location,
1161
v.name!.copy(),
1162
v.type_expression.copy(),
1163
false,
1164
true,
1165
null
1166
);
1167
1168
result.mark_argument();
1169
1170
return result;
1171
si
1172
1173
_copy_variable_list(list: Trees.Variables.LIST) -> Trees.Variables.LIST is
1174
let copied = Collections.LIST[Trees.Variables.VARIABLE]();
1175
1176
for v in list do
1177
copied.add(_copy_variable(v));
1178
od
1179
1180
return Trees.Variables.LIST(list.location, copied);
1181
si
1182
si
1183
si