Skip to content
← Back

src/semantic/types/intersection.ghul

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