Skip to content
← Back

src/semantic/constraint.ghul

1
namespace Semantic is
2
use System.Text.StringBuilder
3
4
use Semantic.Types.Type
5
6
// A constraint recorded against an INFERRED_VARIABLE_TYPE
7
// placeholder's origin during the body-retry walk. Each
8
// constraint captures one operation site that consumed a
9
// placeholder-typed value and that the eventual resolved type
10
// must satisfy. The constraint-aware LUB uses these to filter
11
// candidate types: a candidate is only a valid resolution for
12
// the placeholder if every accumulated constraint discharges
13
// against it.
14
//
15
// This sits alongside `Symbols.Variable._lub_map` (which holds
16
// type-bound constraints). The two together — types we know
17
// the placeholder must be *compatible with*, and operations
18
// the resolved type must *support* — give the solver the full
19
// picture of how the placeholder is used across the function
20
// body without needing a separate dataflow pass.
21
//
22
// Subclasses MUST override `equals` and `get_hash_code` so
23
// that two constraints emitted from semantically equivalent
24
// operation sites collapse into a single set entry. The
25
// Variable-side accumulator stores constraints in a
26
// `Collections.SET` which uses .NET's `Object.Equals` /
27
// `Object.GetHashCode` contract; ghūl exposes these as
28
// `equals(other: object?) -> bool` and `get_hash_code() -> int`.
29
class Constraint abstract is
30
init() is
31
super.init()
32
si
33
34
// Returns true iff `candidate` carries the operation this
35
// constraint requires. Conservative by contract:
36
// subclasses MUST return false rather than risk a false
37
// positive, because a false positive lets the LUB pick a
38
// candidate that subsequently emits broken IL or fails
39
// member lookup in generated code.
40
try_discharge(candidate: Type?) -> bool => false
41
42
// Base class disagrees with everything. Subclasses
43
// override and return true only for same-subclass +
44
// same-field matches. `equals` is the operator the SET
45
// accumulator uses for dedup; `get_hash_code` must agree.
46
equals(other: object?) -> bool => false
47
48
get_hash_code() -> int => 0
49
50
to_string() -> string => "<constraint>"
51
52
// Returns the closed-root Classy referenced by `candidate`,
53
// or null if candidate isn't a NAMED wrapping a closed root
54
// (possibly through a Symbols.GENERIC). Closed-root shapes
55
// are union or ghūl-declared closed class. Used by subclasses
56
// whose discharge accepts a wide root when any subtype
57
// satisfies the constraint — see
58
// _discharge_against_closed_subtypes.
59
_try_get_closed_root_classy(candidate: Types.Type) -> Symbols.Classy? is
60
let named = cast Types.NAMED?(candidate)
61
if !named? then
62
return null
63
fi
64
65
let classy = cast Symbols.Classy?(named.symbol.unspecialized_symbol)
66
if !classy? \/ !classy.is_closed_root then
67
return null
68
fi
69
70
return classy
71
si
72
73
// Returns true iff `candidate` is a closed root whose any
74
// subtype (union variant or closed-class subclass) satisfies
75
// the subclass's `_subtype_matches`. Subtype-specific shape
76
// isn't visible on the wide root itself — it's only reachable
77
// inside an `if .is_X then` narrow. Constraints recorded on
78
// a still-placeholder receiver in a context where narrowing
79
// couldn't fire accept the wide root when at least one
80
// subtype carries the shape; once the placeholder resolves
81
// the subtype becomes reachable at the use site via
82
// narrowing.
83
_discharge_against_closed_subtypes(candidate: Types.Type) -> bool is
84
let root_classy = _try_get_closed_root_classy(candidate)
85
if !root_classy? then
86
return false
87
fi
88
89
for subtype in root_classy.closed_alternatives do
90
if _subtype_matches(subtype) then
91
return true
92
fi
93
od
94
95
return false
96
si
97
98
// Default = no subtype ever matches. Overridden by
99
// subclasses whose discharge accepts wide closed roots via
100
// any-subtype logic.
101
_subtype_matches(subtype_classy: Symbols.Classy) -> bool => false
102
si
103
104
// The placeholder must resolve to a type that has a member
105
// named `member_name`. Emitted at MEMBER-expression visit
106
// sites when the receiver is an INFERRED_VARIABLE_TYPE.
107
//
108
// `call_arity = -1` (the default) leaves discharge name-only.
109
// When the MEMBER expression is the LHS of a CALL, the
110
// CALL-site emits an additional MEMBER_CONSTRAINT carrying the
111
// call's arg-count so the resolved member must be callable at
112
// that arity — useful when a name collides across types but
113
// the arities don't (e.g. `o.add(x)` excludes a type whose
114
// only `add` is two-arg). Type-aware refinement is what
115
// `CALL_CONSTRAINT` captures separately; this is the cheap
116
// structural check.
117
class MEMBER_CONSTRAINT: Constraint is
118
member_name: string
119
call_arity: int
120
121
init(member_name: string) is
122
super.init()
123
self.member_name = member_name
124
self.call_arity = -1
125
si
126
127
init(member_name: string, call_arity: int) is
128
super.init()
129
self.member_name = member_name
130
self.call_arity = call_arity
131
si
132
133
try_discharge(candidate: Type?) -> bool is
134
if !candidate? \/ candidate.is_sentinel then
135
return false
136
fi
137
138
let direct = candidate.find_member(member_name)
139
140
if direct? then
141
return check_arity(direct)
142
fi
143
144
return _discharge_against_closed_subtypes(candidate)
145
si
146
147
_subtype_matches(subtype_classy: Symbols.Classy) -> bool is
148
let m = subtype_classy.find_member(member_name)
149
return m? /\ check_arity(m)
150
si
151
152
// call_arity = -1 means "no arity check"; any found member
153
// passes. Otherwise we accept only when the symbol resolves
154
// to a callable that takes call_arity arguments: a Function
155
// with matching arg count, a FUNCTION_GROUP whose
156
// overload set contains a matching arity, or any other
157
// kind (Property / Field carrying a callable type) — we
158
// can't statically verify those without resolving the
159
// field's type, so we let them through and trust the
160
// overload resolver at the call site to catch a real
161
// mismatch.
162
check_arity(member: Symbols.Symbol) -> bool is
163
if call_arity < 0 then
164
return true
165
fi
166
167
let function = cast Symbols.Function?(member)
168
if function? /\ function.are_arguments_declared then
169
return function.arguments.count == call_arity
170
fi
171
172
if let group: Symbols.FUNCTION_GROUP = member then
173
for f in group.functions do
174
if f.are_arguments_declared /\ f.arguments.count == call_arity then
175
return true
176
fi
177
od
178
return false
179
fi
180
181
return true
182
si
183
184
// Two member constraints collide iff they share a member
185
// name AND arity. An arity-aware emission is strictly more
186
// restrictive than the name-only version, so we keep both
187
// in the set; the candidate must discharge each.
188
equals(other: object?) -> bool is
189
if !other? \/ !isa MEMBER_CONSTRAINT(other) then
190
return false
191
fi
192
193
let m = other
194
return member_name == m.member_name /\ call_arity == m.call_arity
195
si
196
197
get_hash_code() -> int =>
198
member_name.get_hash_code() * 31 + call_arity
199
200
to_string() -> string =>
201
if call_arity < 0 then
202
"member {member_name}"
203
else
204
"member {member_name}/{call_arity}"
205
fi
206
si
207
208
// The placeholder must resolve to a type that is callable
209
// with the captured argument types. Emitted at `_visit(call)`
210
// when the receiver is itself an INFERRED_VARIABLE_TYPE —
211
// i.e., a local lambda or local-bound function is invoked
212
// before its type is pinned, including self-applied shapes
213
// like `(f, x) => f(f(x))`.
214
//
215
// Discharge is "is this a Function/Action type whose argument
216
// count matches and whose arg-types accept the captured
217
// actuals". The shape-side check is strict (heads must be
218
// Function/Action) — we don't accept arbitrary types with an
219
// `invoke` member.
220
//
221
// The captured argument types may themselves contain
222
// placeholders (the inner `f(x)` above supplies `x`'s
223
// placeholder as its single arg). Per-slot check looks through
224
// placeholder args via `_resolved_arg_type` and skips slots
225
// whose actual is still unresolved — an unresolved placeholder
226
// doesn't refute the candidate. On the next retry iteration
227
// the placeholder's origin may have settled, the look-through
228
// substitutes its resolved type, and the slot compares
229
// cleanly. Resolved-into-concrete and concrete-original args
230
// are full-compared as before.
231
class CALL_CONSTRAINT: Constraint is
232
argument_types: Collections.List[Type] public
233
234
init(argument_types: Collections.List[Type]) is
235
super.init()
236
self.argument_types = argument_types
237
si
238
239
try_discharge(candidate: Type?) -> bool is
240
if !candidate? \/ candidate.is_sentinel then
241
return false
242
fi
243
244
if !candidate.is_function /\ !candidate.is_action then
245
return false
246
fi
247
248
// Function carries one extra slot for the return type;
249
// Action carries arg-slots only. Compare just the
250
// argument prefix.
251
let candidate_args =
252
if isa Types.NAMED(candidate) then
253
candidate.arguments
254
else
255
null
256
fi
257
258
if !candidate_args? then
259
return false
260
fi
261
262
let expected_arg_count =
263
if candidate.is_function then
264
candidate_args.count - 1
265
else
266
candidate_args.count
267
fi
268
269
if expected_arg_count != argument_types.count then
270
return false
271
fi
272
273
// Per-slot check with placeholder look-through:
274
//
275
// - concrete actual — compare against the
276
// candidate's formal; if it doesn't fit, the
277
// candidate is wrong → reject.
278
//
279
// - placeholder actual whose origin has settled —
280
// `_resolved_arg_type` substitutes the origin's
281
// resolved type and compare. Picks up inner-lambda
282
// call args (`f(x)` captures x while x is still a
283
// phantom; x later resolves via call-site match propagation,
284
// but the captured placeholder instance in this
285
// constraint's args still reads as "inferred"
286
// without the look-through).
287
//
288
// - placeholder actual whose origin is still unresolved —
289
// skip this slot. The candidate isn't refuted by an
290
// unresolved placeholder — once the placeholder's
291
// origin settles on a later iter the discharge re-
292
// runs and compares cleanly.
293
//
294
// Formal is resolved through `_resolved_arg_type` too so the
295
// candidate's args — themselves potentially placeholder-
296
// instances whose origins have settled — read as their
297
// concrete type for the per-slot compare.
298
for i in 0..argument_types.count do
299
let formal = _resolved_arg_type(candidate_args[i])
300
let raw_actual = argument_types[i]
301
let actual = _resolved_arg_type(raw_actual)
302
303
if !actual.contains_inferred then
304
if cast int(formal.compare(actual)) > cast int(Types.MATCH.CONVERTABLE) then
305
return false
306
fi
307
fi
308
od
309
310
return true
311
si
312
313
// Follow an INFERRED_VARIABLE_TYPE arg through to its origin
314
// symbol's resolved type when settled. Returns the input
315
// unchanged for non-placeholder args, or for a placeholder
316
// whose origin hasn't resolved yet.
317
_resolved_arg_type(a: Type) -> Type is
318
if !isa Types.INFERRED_VARIABLE_TYPE(a) then
319
return a
320
fi
321
322
let placeholder = cast Types.INFERRED_VARIABLE_TYPE(a)
323
324
if !placeholder.origin.type? then
325
return a
326
fi
327
328
let resolved = placeholder.origin.type
329
330
if resolved.is_settled then
331
return resolved
332
fi
333
334
return a
335
si
336
337
equals(other: object?) -> bool is
338
if !other? \/ !isa CALL_CONSTRAINT(other) then
339
return false
340
fi
341
342
let c = other
343
344
if argument_types.count != c.argument_types.count then return false; fi
345
346
for i in 0..argument_types.count do
347
let a = argument_types[i]
348
let b = c.argument_types[i]
349
350
if !(a.matches(b)) then return false; fi
351
od
352
353
return true
354
si
355
356
// Hash on arity + each arg's hash. Arg `matches` returns true
357
// for INFERRED_VARIABLE_TYPE against anything, so two
358
// constraints with placeholder args still collide
359
// structurally even when their placeholders differ —
360
// that matches the de-duplication policy for re-walks of
361
// the same call site.
362
get_hash_code() -> int is
363
let h mut = 17
364
365
h = h * 31 + argument_types.count
366
367
for a in argument_types do
368
h = h * 31 + a.get_hash_code()
369
od
370
371
return h
372
si
373
374
to_string() -> string is
375
let buffer = StringBuilder()
376
buffer.append("call(")
377
let first mut = true
378
for a in argument_types do
379
if !first then buffer.append(", "); fi
380
first = false
381
buffer.append(a.to_string())
382
od
383
buffer.append(")")
384
return buffer.to_string()
385
si
386
si
387
388
// The placeholder must resolve to a type that can be
389
// destructured into at least `member_count` positions —
390
// either a value-tuple of the right arity, or any other type
391
// whose `find_destructure_member(0..member_count-1)` all
392
// return non-null. Emitted at destructure-binding sites
393
// (`let (a, b) = p`) when the RHS type is still an
394
// INFERRED_VARIABLE_TYPE so the body-retry loop has a basis
395
// to filter candidate types instead of bailing out with
396
// "cannot destructure" on a placeholder.
397
class DESTRUCTURE_CONSTRAINT: Constraint is
398
member_count: int public
399
400
init(member_count: int) is
401
super.init()
402
self.member_count = member_count
403
si
404
405
try_discharge(candidate: Type?) -> bool is
406
if !candidate? \/ candidate.is_sentinel then
407
return false
408
fi
409
410
if has_destructure_shape(candidate) then
411
return true
412
fi
413
414
return _discharge_against_closed_subtypes(candidate)
415
si
416
417
_subtype_matches(subtype_classy: Symbols.Classy) -> bool =>
418
has_destructure_shape(subtype_classy.type)
419
420
// True iff `t` directly satisfies the destructure shape —
421
// a value tuple of matching arity, or a type whose
422
// `find_destructure_member(0..N-1)` all return non-null.
423
// Used both for the direct candidate and for each subtype
424
// when the candidate is a closed root.
425
has_destructure_shape(t: Type?) -> bool is
426
if !t? then
427
return false
428
fi
429
430
if t.is_value_tuple then
431
return t.arguments.count == member_count
432
fi
433
434
for i in 0..member_count do
435
if !t.find_destructure_member(i)? then
436
return false
437
fi
438
od
439
440
return true
441
si
442
443
equals(other: object?) -> bool is
444
if !other? \/ !isa DESTRUCTURE_CONSTRAINT(other) then
445
return false
446
fi
447
448
let d = other
449
return member_count == d.member_count
450
si
451
452
get_hash_code() -> int => member_count
453
454
to_string() -> string => "destructure/{member_count}"
455
si
456
457
// The placeholder must resolve to a type the for-loop expression
458
// path treats as iterable: either it carries `move_next` (the
459
// direct iterator shape) or `$get_iterator` (the iterable
460
// shape — a property returning an iterator). Emitted at
461
// `for x in t` sites when `t`'s type is still an
462
// `INFERRED_VARIABLE_TYPE` so the body-retry loop can filter
463
// candidate types by iterability instead of letting set_iterator_for
464
// fire a hard "not iterable" on a placeholder it couldn't have
465
// checked anyway.
466
//
467
// Carries no payload — every iterable constraint at every
468
// for-loop site is structurally identical, so equality / hash
469
// are constant and the SET accumulator dedupes to a single
470
// entry per placeholder.
471
class ITERABLE_CONSTRAINT: Constraint is
472
init() is
473
super.init()
474
si
475
476
try_discharge(candidate: Type?) -> bool is
477
if !candidate? \/ candidate.is_sentinel then
478
return false
479
fi
480
481
if has_iterable_shape(candidate) then
482
return true
483
fi
484
485
return _discharge_against_closed_subtypes(candidate)
486
si
487
488
_subtype_matches(subtype_classy: Symbols.Classy) -> bool =>
489
has_iterable_shape(subtype_classy.type)
490
491
has_iterable_shape(t: Type?) -> bool is
492
if !t? then
493
return false
494
fi
495
496
if t.find_member("move_next")? then
497
return true
498
fi
499
500
if t.find_member("$get_iterator")? then
501
return true
502
fi
503
504
return false
505
si
506
507
equals(other: object?) -> bool =>
508
other? /\ isa ITERABLE_CONSTRAINT(other)
509
510
get_hash_code() -> int => 23
511
512
to_string() -> string => "iterable"
513
si
514
515
// The placeholder must resolve to a type that admits indexing
516
// by `index_type` — i.e. exposes either the indexer read
517
// accessor (auto-synthesised on classes with an indexer, and
518
// the name an imported indexer is declared under) or an
519
// explicit lower-case `get_item(index)` member whose
520
// first formal accepts `index_type`. Emitted at `t[i]` sites
521
// when `t`'s type is still an `INFERRED_VARIABLE_TYPE`.
522
//
523
// The index_type itself may contain inference placeholders;
524
// discharge defers conservatively in that case so the LUB
525
// gets to re-check on the next iteration once placeholders
526
// resolve.
527
class INDEX_CONSTRAINT: Constraint is
528
index_type: Type public
529
530
init(index_type: Type) is
531
super.init()
532
self.index_type = index_type
533
si
534
535
try_discharge(candidate: Type?) -> bool is
536
if !candidate? \/ candidate.is_sentinel then
537
return false
538
fi
539
540
if !index_type.is_settled then
541
return false
542
fi
543
544
if has_indexable_shape(candidate) then
545
return true
546
fi
547
548
return _discharge_against_closed_subtypes(candidate)
549
si
550
551
_subtype_matches(subtype_classy: Symbols.Classy) -> bool =>
552
has_indexable_shape(subtype_classy.type)
553
554
has_indexable_shape(t: Type?) -> bool is
555
if !t? then
556
return false
557
fi
558
559
// The lower-case spelling catches a hand-written
560
// `get_item` method, which indexes nothing but reads as
561
// an indexer to a caller filtering candidate types.
562
return is_indexer_match(t, Symbols.INDEXER_NAMES.read) \/ is_indexer_match(t, "get_item")
563
si
564
565
// True iff `t` exposes a `member_name` callable whose first
566
// formal accepts `index_type`. Function or FUNCTION_GROUP
567
// surface; other kinds aren't considered indexers.
568
is_indexer_match(t: Type, member_name: string) -> bool is
569
let member = t.find_member(member_name)
570
if !member? then
571
return false
572
fi
573
574
if let function: Symbols.Function = member then
575
return accepts_index(function)
576
fi
577
578
if let group: Symbols.FUNCTION_GROUP = member then
579
for f in group.functions do
580
if accepts_index(f) then
581
return true
582
fi
583
od
584
return false
585
fi
586
587
return false
588
si
589
590
accepts_index(f: Symbols.Function) -> bool is
591
if !f.are_arguments_declared \/ f.arguments.count == 0 then
592
return false
593
fi
594
595
let formal = f.arguments[0]
596
if formal.is_error then
597
return false
598
fi
599
600
return formal.is_assignable_from(index_type)
601
si
602
603
equals(other: object?) -> bool is
604
if !other? \/ !isa INDEX_CONSTRAINT(other) then
605
return false
606
fi
607
608
let i = other
609
return index_type.matches(i.index_type)
610
si
611
612
get_hash_code() -> int => index_type.get_hash_code() * 31 + 29
613
614
to_string() -> string => "index[{index_type}]"
615
si
616
si