Skip to content
← Back

src/semantic/types/intersection.ghul

1
namespace Semantic.Types is
2
use System.Text.StringBuilder;
3
4
// Compiler-internal "value is A AND B (AND ...)" representation,
5
// produced only by the narrowing pass when `isa T(x: D)` fires
6
// and neither type strict-subtypes the other. Tracks both the
7
// declared-side type and each isa-target so member lookup can
8
// see members from any side.
9
//
10
// Never user-spellable, never appears in slot types (fields,
11
// arguments, return types, generic-argument positions), never
12
// reaches IL emission or reflection metadata. Reads of a narrowed
13
// variable in IL load against the variable's declared type — the
14
// intersection is purely a compile-time member-resolution and
15
// assignability fact.
16
//
17
// Invariants (enforced by `INTERSECTION.create`):
18
// - At most one class / struct / union member (the runtime
19
// value has exactly one concrete identity).
20
// - Zero or more trait members.
21
// - No duplicate members (`matches` identity).
22
// - No redundant supertype members — if A and B are both in
23
// the list and B subtypes A, A is dropped (B implies A).
24
// - If after de-dup / supertype-drop only one member remains,
25
// `create` returns that plain type, never a singleton
26
// INTERSECTION.
27
// - Canonical order: class/struct/union first (if any), then
28
// traits sorted by `to_string`. Lets `matches` be element-wise.
29
//
30
// Design note: `docs/claude/intersection-types.md`.
31
class INTERSECTION: NAMED is
32
_members: Collections.LIST[Type];
33
34
members: Collections.Iterable[Type] => _members;
35
members_count: int => _members.count;
36
37
// The class / struct / union member, if any. The intersection's
38
// "concrete identity" for queries that need a single CLR-type
39
// answer. Null when every member is a trait.
40
class_side: Type? is
41
for m in _members do
42
if _is_concrete_kind(m) then
43
return m;
44
fi
45
od
46
return null;
47
si
48
49
// Default delegation target for Type-level queries that don't
50
// have a defined intersection semantics: class side if any,
51
// first trait otherwise. Members list is non-empty by
52
// construction so this never reads `_members[0]` on an empty
53
// list.
54
representative: Type =>
55
let cs = class_side in
56
if cs? then cs else _members[0] fi;
57
58
// `_build` orders members class-side first (if any), then
59
// traits — so members[0] is the natural CLR-level identity
60
// for any NAMED-shaped query (gen_type, etc).
61
init(members: Collections.LIST[Type]) is
62
super.init(members[0].symbol);
63
_members = members;
64
si
65
66
// Build the intersection of `a` and `b`. Applies de-dup,
67
// supertype-drop, singleton collapse, and canonical ordering.
68
// Returns a plain Type (one of the inputs, or the surviving
69
// member after collapse) when no real intersection is needed.
70
create(a: Type, b: Type) -> Type static is
71
let result = try_create(a, b);
72
73
assert result?
74
else "intersection contains more than one class/struct/union member";
75
76
return result;
77
si
78
79
// Like `create`, but returns null when `a` and `b` carry
80
// unrelated concrete identities (two classes with no subtype
81
// relation, a struct and a class, ...) — no runtime value can
82
// be both, so no intersection exists. Callers that can reach
83
// that combination (narrow stacking on a statically-impossible
84
// test edge) use this and decide what the empty intersection
85
// means for them.
86
try_create(a: Type, b: Type) -> Type? static is
87
let collected = Collections.LIST[Type]();
88
89
_collect(a, collected);
90
_collect(b, collected);
91
92
return _try_build(collected);
93
si
94
95
// True for types that can be the "concrete identity" of a
96
// runtime value: classes, structs, value types, unions,
97
// variants. At most one of these may appear in an intersection.
98
_is_concrete_kind(t: Type) -> bool static =>
99
t.is_class
100
\/ t.is_value_type
101
\/ t.symbol.is_union;
102
103
// Flatten `t` into `into`, applying _add_member's
104
// de-dup / supertype-drop rules. Skips ERROR / inferred /
105
// null inputs.
106
_collect(t: Type?, into: Collections.LIST[Type]) static is
107
if !t? \/ t.is_error \/ t.is_inferred then
108
return;
109
fi
110
111
if let inner: INTERSECTION = t then
112
for m in inner.members do
113
_add_member(m, into);
114
od
115
else
116
_add_member(t, into);
117
fi
118
si
119
120
// Add `m` to `into` with redundancy elimination.
121
// - If an existing member subtypes `m` (existing implies
122
// `m`), `m` is redundant — skip.
123
// - Otherwise, drop every existing member that `m` subtypes
124
// (more-specific `m` supersedes them).
125
// - Then add `m`.
126
//
127
// Subtype tests strip optional layers from both sides.
128
// Intersection members represent a runtime value's identity,
129
// and the at-most-one-concrete invariant is about that runtime
130
// identity, not its declared optionality. With the strict
131
// non-nullable-by-default rule, comparing `A` to `B?` directly
132
// would return false on the subtype check and miss real
133
// redundancies — the dedup needs to see the bare relationship.
134
//
135
// Precision-vs-soundness tradeoff: when A is non-optional
136
// and B? subtypes A bare, this drops A and keeps B? — the
137
// narrower result would be `B` (non-null), but the wider B?
138
// is sound (rejects more code than necessary, never accepts
139
// unsound code). Could be tightened by preserving the
140
// optional flag at the more-specific position, but the
141
// current code paths don't seem to exercise the case.
142
_add_member(m: Type, into: Collections.LIST[Type]) static is
143
let m_bare = _strip_optional(m);
144
145
for existing in into do
146
if existing.matches(m) then
147
return;
148
fi
149
150
if m_bare.is_assignable_from(_strip_optional(existing)) then
151
return;
152
fi
153
od
154
155
let kept = Collections.LIST[Type]();
156
157
for existing in into do
158
if !_strip_optional(existing).is_assignable_from(m_bare) then
159
kept.add(existing);
160
fi
161
od
162
163
into.clear();
164
165
for k in kept do
166
into.add(k);
167
od
168
169
into.add(m);
170
si
171
172
_strip_optional(t: Type) -> Type static =>
173
if t.is_optional then
174
t.as_non_optional()
175
else
176
t
177
fi;
178
179
// Final assembly: validate the at-most-one-concrete invariant
180
// (null when it fails — no value can carry two unrelated
181
// concrete identities), sort canonically, collapse singleton
182
// to plain type.
183
_try_build(members: Collections.LIST[Type]) -> Type? static is
184
if members.count == 0 then
185
return ERROR();
186
fi
187
188
if members.count == 1 then
189
return members[0];
190
fi
191
192
let concrete_count mut = 0;
193
194
for m in members do
195
if _is_concrete_kind(m) then
196
concrete_count = concrete_count + 1;
197
fi
198
od
199
200
if concrete_count > 1 then
201
return null;
202
fi
203
204
let ordered = Collections.LIST[Type]();
205
let traits = Collections.LIST[Type]();
206
207
for m in members do
208
if _is_concrete_kind(m) then
209
ordered.add(m);
210
else
211
traits.add(m);
212
fi
213
od
214
215
// Order the traits by their fully-qualified names so the
216
// canonical ordering (which makes `matches` element-wise) does not
217
// depend on the scope names happen to be rendered relative to.
218
let use render_scope = IoC.CONTAINER.instance.name_display.with_scope(null);
219
220
for i in 0..traits.count do
221
for j in (i + 1)..traits.count do
222
if traits[i].to_string()!.compare_to(traits[j].to_string()) > 0 then
223
let temp = traits[i];
224
traits[i] = traits[j];
225
traits[j] = temp;
226
fi
227
od
228
od
229
230
for t in traits do
231
ordered.add(t);
232
od
233
234
return INTERSECTION(ordered);
235
si
236
237
// ===== Type-property delegation =====
238
//
239
// Most queries that ask "what kind of type is this?" delegate
240
// to the representative (class side, or first trait). The
241
// intersection's runtime value behaves like the class side
242
// for CLR-level queries (gen_type, gen_class_name); for
243
// type-system queries that have a defined intersection
244
// semantics (is_assignable_from, find_member) we override.
245
246
symbol: Symbols.Symbol => representative.symbol;
247
248
is_value_type: bool => false;
249
is_class: bool => class_side?;
250
is_trait: bool => !class_side?;
251
is_named: bool => true;
252
253
is_inheritable: bool => representative.is_inheritable;
254
is_object: bool => false;
255
is_root_value_type: bool => false;
256
is_void: bool => false;
257
is_action: bool => false;
258
is_function: bool => false;
259
is_ref: bool => false;
260
is_type_variable: bool => false;
261
is_value_tuple: bool => false;
262
is_optional: bool => false;
263
264
as_optional() -> Type => self;
265
as_non_optional() -> Type => self;
266
267
// ===== Equality and hashing =====
268
//
269
// Element-wise compare in canonical order. Two intersections
270
// with the same membership match regardless of construction
271
// path because the factory enforces canonical ordering.
272
273
matches(other: Type) -> bool is
274
if !isa INTERSECTION(other) then
275
return false;
276
fi
277
278
let o = other;
279
280
if o._members.count != _members.count then
281
return false;
282
fi
283
284
for i in 0.._members.count do
285
if !_members[i].matches(o._members[i]) then
286
return false;
287
fi
288
od
289
290
return true;
291
si
292
293
get_hash_code() -> int is
294
let h mut = 0;
295
296
for m in _members do
297
h = h * 31 + m.get_hash_code();
298
od
299
300
return h;
301
si
302
303
// ===== Assignability =====
304
//
305
// A value of type `D & T1 & T2 ...` IS each of D, T1, T2 ...
306
// (D & T1 & ...).is_assignable_from(X)
307
// iff X is assignable to every member.
308
// X.is_assignable_from(D & T1 & ...)
309
// iff X is assignable to any member
310
// (handled by the existing dispatch — each member's
311
// compare(X) finds X among its supertypes when
312
// applicable).
313
314
is_assignable_from(other: Type) -> bool is
315
for m in _members do
316
if !m.is_assignable_from(other) then
317
return false;
318
fi
319
od
320
321
return true;
322
si
323
324
compare(other: Type) -> MATCH is
325
let worst mut = MATCH.SAME;
326
327
for m in _members do
328
let c = m.compare(other);
329
330
if c == MATCH.DIFFERENT then
331
return MATCH.DIFFERENT;
332
fi
333
334
if cast int(c) > cast int(worst) then
335
worst = c;
336
fi
337
od
338
339
return worst;
340
si
341
342
// ===== Member lookup =====
343
//
344
// Walk each member, collect the non-null lookups. Resolution:
345
// - 0 results → null
346
// - 1 result → that result
347
// - all FUNCTION_GROUPs → merge (same shape as
348
// classy.find_enclosing, dedup by
349
// override_class)
350
// - mixed kinds → class-side wins for non-function
351
// members (per design's tie-break);
352
// else first trait wins (pragmatic
353
// fallback for multi-trait clash —
354
// documented limitation, matches
355
// existing classy.find_enclosing
356
// non-function handling)
357
358
find_member(name: string) -> Symbols.Symbol? is
359
let class_member: Symbols.Symbol? mut = null;
360
let trait_members = Collections.LIST[Symbols.Symbol]();
361
let cs = class_side;
362
363
for m in _members do
364
let r = m.find_member(name);
365
366
if r? then
367
if cs? /\ m == cs then
368
class_member = r;
369
else
370
trait_members.add(r);
371
fi
372
fi
373
od
374
375
if !class_member? /\ trait_members.count == 0 then
376
return null;
377
fi
378
379
if class_member? /\ trait_members.count == 0 then
380
return class_member;
381
fi
382
383
if !class_member? /\ trait_members.count == 1 then
384
return trait_members[0];
385
fi
386
387
let all_groups mut = true;
388
389
if class_member? /\ !isa Symbols.FUNCTION_GROUP(class_member) then
390
all_groups = false;
391
fi
392
393
for t in trait_members do
394
if !isa Symbols.FUNCTION_GROUP(t) then
395
all_groups = false;
396
fi
397
od
398
399
if all_groups then
400
return _merge_function_groups(class_member, trait_members, name);
401
fi
402
403
if class_member? then
404
return class_member;
405
fi
406
407
return trait_members[0];
408
si
409
410
// Build a combined FUNCTION_GROUP from `class_member`
411
// (optional) and `trait_members`. Dedupe by `override_class`
412
// so the same method declared by the same trait doesn't
413
// appear twice when reached via multiple paths. Functions
414
// without an override_class — typically synthesized — are
415
// added unconditionally; collisions there are not expected
416
// in practice.
417
_merge_function_groups(
418
class_member: Symbols.Symbol?,
419
trait_members: Collections.LIST[Symbols.Symbol],
420
name: string
421
) -> Symbols.Symbol is
422
let sample =
423
if class_member? then
424
class_member
425
else
426
trait_members[0]
427
fi;
428
429
let combined = Symbols.FUNCTION_GROUP(sample.location, sample.owner!, name);
430
let seen = Collections.SET[METHOD_OVERRIDE_CLASS]();
431
432
_absorb_function_group(class_member, combined, seen);
433
434
for t in trait_members do
435
_absorb_function_group(t, combined, seen);
436
od
437
438
return combined;
439
si
440
441
_absorb_function_group(
442
source: Symbols.Symbol?,
443
into: Symbols.FUNCTION_GROUP,
444
seen: Collections.SET[METHOD_OVERRIDE_CLASS]
445
) static is
446
if !source? then
447
return;
448
fi
449
450
if let group: Symbols.FUNCTION_GROUP = source then
451
for f in group.functions do
452
if !seen.contains(f.override_class) then
453
seen.add(f.override_class);
454
into.add(f);
455
fi
456
od
457
fi
458
si
459
460
// ===== Display =====
461
//
462
// Plain text join with ` & `. Single canonical form used in
463
// error messages, hovers, snapshots, debug dumps.
464
465
to_string() -> string is
466
let buffer = StringBuilder();
467
let first mut = true;
468
469
for m in _members do
470
if !first then
471
buffer.append(" & ");
472
fi
473
474
buffer.append(m.to_string());
475
first = false;
476
od
477
478
return buffer.to_string();
479
si
480
481
short_description: string => to_string();
482
483
// ===== IL emission boundary =====
484
//
485
// Intersections never appear in slot types — narrowed-variable
486
// reads load against the variable's declared type, not the
487
// narrowed view. These shouldn't normally execute; if they
488
// do, emit the representative's IL (the concrete CLR-level
489
// identity).
490
491
gen_type(buffer: StringBuilder) is
492
representative.gen_type(buffer);
493
si
494
495
gen_class_name(buffer: StringBuilder) is
496
representative.gen_class_name(buffer);
497
si
498
499
walk(action: (Type) -> void) is
500
for m in _members do
501
m.walk(action);
502
od
503
504
action(self);
505
si
506
507
// ===== Specialization =====
508
//
509
// Intersections shouldn't appear in generic-argument
510
// positions, so this path is defensive. If it fires,
511
// specialize each member and rebuild through the factory
512
// to preserve invariants.
513
514
specialize(type_map: Collections.Map[string,Type]) -> Type is
515
if _members.count < 2 then
516
return self;
517
fi
518
519
let result mut = _members[0].specialize(type_map);
520
521
for i in 1.._members.count do
522
result = INTERSECTION.create(result, _members[i].specialize(type_map));
523
od
524
525
return result;
526
si
527
si
528
si