Skip to content
← Back

src/syntax/process/rewrite-syntax-trees/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
// The @pragma(...) chain wrapping the user's `init(..)`, if
276
// any - outermost first, reattached around the synthesised
277
// primary init below so every pragma written against the
278
// user's declaration (there can be more than one stacked)
279
// still reaches wherever that constructor's diagnostics are
280
// anchored, rather than being discarded along with the node
281
// it wrapped.
282
let user_primary_init_pragmas: Collections.LIST[Trees.Definitions.PRAGMA]? mut = null
283
let secondary_inits = Collections.LIST[Trees.Definitions.FUNCTION]()
284
let secondary_init_pragmas = Collections.MAP[Trees.Definitions.FUNCTION, Collections.LIST[Trees.Definitions.PRAGMA]]()
285
let kept = Collections.LIST[Trees.Definitions.Definition]()
286
287
// Auto-gen props ride at the head of the body so they precede
288
// user-written members, matching the order of the primary
289
// header.
290
for synthesised in auto_gen_props do
291
captures.add(synthesised)
292
kept.add(synthesised)
293
od
294
295
for d in classy.body do
296
if isa Trees.Definitions.SUPER_CALL(d) then
297
let sc = cast Trees.Definitions.SUPER_CALL(d)
298
299
if super_call? then
300
_logger.error(sc.location, "duplicate super(...) declaration")
301
else
302
super_call = sc
303
fi
304
elif _has_init_modifier(d) then
305
_logger.error(
306
d.location,
307
"init modifier is only valid on a primary constructor parameter"
308
)
309
kept.add(d)
310
elif isa Trees.Definitions.PROPERTY(d) /\ chosen_captures.contains(cast Trees.Definitions.PROPERTY(d)) then
311
let capture = cast Trees.Definitions.PROPERTY(d)
312
captures.add(capture)
313
kept.add(d)
314
elif _is_unmatched_capture_shorthand(d) then
315
// Field-shaped property with INFER type but no
316
// matching primary parameter (or the matching one
317
// was already captured by `_foo`, leaving the bare
318
// `foo;` shorthand stranded with no type to fall
319
// back to). Either way the field can't reach
320
// downstream phases — flag it and drop it.
321
let p = cast Trees.Definitions.PROPERTY?(d)!
322
let stripped = _strip_underscore(p.name!.name)
323
324
if already_captured_param_names.contains(stripped) then
325
_logger.error(
326
p.location,
327
"primary parameter {stripped} is already captured by _{stripped}"
328
)
329
else
330
_logger.error(
331
p.location,
332
"no primary parameter named {stripped} to capture"
333
)
334
fi
335
elif _is_init_function(d.without_pragmas) then
336
let f = cast Trees.Definitions.FUNCTION?(d.without_pragmas)!
337
let d_pragmas = _collect_pragmas(d)
338
let has_splice = _arg_list_has_splice(f.arguments)
339
340
if has_splice then
341
let splice_count = _count_splices(f.arguments)
342
343
if splice_count > 1 then
344
_logger.error(f.location, "init parameter list contains more than one ..")
345
fi
346
347
if _is_primary_init(f) then
348
if user_primary_init? then
349
_logger.error(f.location, "duplicate primary constructor init body")
350
else
351
user_primary_init = f
352
user_primary_init_pragmas = d_pragmas
353
fi
354
else
355
secondary_inits.add(f)
356
357
if d_pragmas.count > 0 then
358
secondary_init_pragmas[f] = d_pragmas
359
fi
360
fi
361
else
362
kept.add(d)
363
fi
364
else
365
kept.add(d)
366
fi
367
od
368
369
// Fill in INFER type expressions from the chosen primary
370
// parameter. Captures with an explicit type (e.g.
371
// `x: int public;` to widen visibility) keep their type.
372
for capture in captures do
373
let matching = param_for_capture[capture]
374
375
if capture.type_expression.is_inferred then
376
capture.set_type_expression(matching.type_expression.copy())
377
fi
378
od
379
380
// Synthesise the per-capture assignment statements.
381
let auto_statements = Collections.LIST[Trees.Statements.Statement]()
382
383
if super_call? then
384
auto_statements.add(_make_super_init_call(super_call))
385
fi
386
387
for capture in captures do
388
auto_statements.add(_make_capture_assignment(capture, param_for_capture[capture]))
389
od
390
391
// If the user wrote `init(..) is body si`, splice their
392
// statements in after the auto-generated assignments.
393
if user_primary_init? /\ user_primary_init.body? /\ isa Trees.Bodies.BLOCK(user_primary_init.body) then
394
let user_block = cast Trees.Bodies.BLOCK(user_primary_init.body)
395
396
for s in user_block.statements do
397
auto_statements.add(s)
398
od
399
fi
400
401
// The BLOCK's own location is internal: the body has no
402
// source text the user wrote, and giving it the class span
403
// would make the incremental body re-walk treat every
404
// interface symbol on the class header (the class itself,
405
// its auto-properties) as `inside` this body and drop them
406
// from the symbol-definition map after an EDIT.
407
let primary_init_body =
408
Trees.Bodies.BLOCK(
409
LOCATION.internal,
410
Trees.Statements.LIST(LOCATION.internal, auto_statements)
411
)
412
413
// Deep-copy the primary parameters into a fresh argument
414
// list for the synthesised init — keeps the original list
415
// available for the secondary-init splice expansion below
416
// without aliasing the same VARIABLE instances across two
417
// FUNCTION nodes.
418
let primary_init_args = _copy_variable_list(primary_params)
419
420
// Anchor the synthesised init's overall span to the class
421
// header (class name through primary parameter list). A
422
// user-written `init(...)` at the same signature picks up
423
// the duplicate-method diagnostic against this range; the
424
// synthesised init also surfaces in the VSCE outline at
425
// the class-header position.
426
//
427
// The name identifier is anchored separately: when the
428
// user wrote `init(..) is ... si`, point it at the user's
429
// own `init` source location so hover, goto-definition,
430
// and the semantic-tokens classifier resolve to this
431
// symbol at the user-typed keyword. Without that, the
432
// user's `init` has no recorded symbol use and falls
433
// back to TextMate colouring with no hover. For the
434
// auto-generated case (no user `init(..)`) the name stays
435
// at the class header.
436
let header_location = classy.name.location :: primary_params.location
437
438
let primary_init_name =
439
if user_primary_init? /\ user_primary_init.name? then
440
user_primary_init.name.copy()
441
else
442
Trees.Identifiers.Identifier(header_location, "init")
443
fi
444
445
let primary_init =
446
Trees.Definitions.FUNCTION(
447
header_location,
448
primary_init_name,
449
Trees.TypeExpressions.LIST(LOCATION.internal, Collections.LIST[Trees.TypeExpressions.TypeExpression](0)),
450
primary_init_args,
451
Trees.TypeExpressions.INFER(class_location),
452
Trees.Modifiers.LIST(class_location, null, null),
453
primary_init_body
454
)
455
456
primary_init.is_primary_constructor = true
457
458
if user_primary_init_pragmas? then
459
kept.add(_rewrap_pragmas(user_primary_init_pragmas, primary_init))
460
else
461
kept.add(primary_init)
462
fi
463
464
// Re-attach each secondary init with its splice expanded
465
// and an implicit chain to the primary init prepended.
466
for f in secondary_inits do
467
_expand_secondary_init(f, primary_params)
468
469
let f_pragmas: Collections.LIST[Trees.Definitions.PRAGMA] mut
470
471
if secondary_init_pragmas.try_get_value(f, f_pragmas ref) then
472
kept.add(_rewrap_pragmas(f_pragmas, f))
473
else
474
kept.add(f)
475
fi
476
od
477
478
// Auto-deconstruct synthesis. When the user has not written a
479
// `deconstruct(...)` method and has not exposed any
480
// conventionally-named positional members (`0`, `1`, ...),
481
// synthesise a `deconstruct` exposing every public-readable
482
// capture in primary-header order. The resolver's precedence
483
// (tuple > deconstruct > positional > by-name) means a
484
// user-supplied `0`/`1`/... continues to be the explicit opt-in
485
// to positional access — co-existing with the synthesised
486
// method would make it unreachable.
487
_maybe_synthesise_deconstruct(
488
classy,
489
class_location,
490
primary_params,
491
param_for_capture,
492
already_captured_param_names,
493
kept
494
)
495
496
// Replace the body's contents with the curated list.
497
classy.body.clear_definitions()
498
499
for d in kept do
500
classy.body.add(d)
501
od
502
503
classy.set_primary_params(null)
504
si
505
506
_find_chosen_capture(
507
param_name: string,
508
field_props_by_name: Collections.MAP[string, Trees.Definitions.PROPERTY],
509
already_chosen: Collections.SET[Trees.Definitions.PROPERTY]
510
) -> Trees.Definitions.PROPERTY? is
511
let underscore_name = "_{param_name}"
512
513
if field_props_by_name.contains_key(underscore_name) then
514
let candidate = field_props_by_name[underscore_name]
515
516
if !already_chosen.contains(candidate) then
517
return candidate
518
fi
519
fi
520
521
if field_props_by_name.contains_key(param_name) then
522
let candidate = field_props_by_name[param_name]
523
524
if !already_chosen.contains(candidate) then
525
return candidate
526
fi
527
fi
528
529
return null
530
si
531
532
_filter_unusable_primary_params(args: Trees.Variables.LIST) -> Trees.Variables.LIST is
533
let kept = Collections.LIST[Trees.Variables.VARIABLE]()
534
535
for v in args do
536
if v.name? then
537
kept.add(v)
538
fi
539
od
540
541
if kept.count == args.count then
542
return args
543
fi
544
545
return Trees.Variables.LIST(args.location, kept)
546
si
547
548
si
549
si