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 `get_Item` /
517
// `get_item` (the convention auto-synthesised on classes with
518
// an indexer) or an explicit `get_item(index)` member whose
519
// first formal accepts `index_type`. Emitted at `t[i]` sites
520
// when `t`'s type is still an `INFERRED_VARIABLE_TYPE`.
521
//
522
// The index_type itself may contain inference placeholders;
523
// discharge defers conservatively in that case so the LUB
524
// gets to re-check on the next iteration once placeholders
525
// resolve.
526
class INDEX_CONSTRAINT: Constraint is
527
index_type: Type public;
528
529
init(index_type: Type) is
530
super.init();
531
self.index_type = index_type;
532
si
533
534
try_discharge(candidate: Type?) -> bool is
535
if !candidate? \/ candidate.is_sentinel then
536
return false;
537
fi
538
539
if !index_type.is_settled then
540
return false;
541
fi
542
543
if has_indexable_shape(candidate) then
544
return true;
545
fi
546
547
return _discharge_against_closed_subtypes(candidate);
548
si
549
550
_subtype_matches(subtype_classy: Symbols.Classy) -> bool =>
551
has_indexable_shape(subtype_classy.type);
552
553
has_indexable_shape(t: Type?) -> bool is
554
if !t? then
555
return false;
556
fi
557
558
// `get_Item` is the .NET-imported convention; `get_item`
559
// is the ghūl-source convention.
560
return is_indexer_match(t, "get_Item") \/ is_indexer_match(t, "get_item");
561
si
562
563
// True iff `t` exposes a `member_name` callable whose first
564
// formal accepts `index_type`. Function or FUNCTION_GROUP
565
// surface; other kinds aren't considered indexers.
566
is_indexer_match(t: Type, member_name: string) -> bool is
567
let member = t.find_member(member_name);
568
if !member? then
569
return false;
570
fi
571
572
if let function: Symbols.Function = member then
573
return accepts_index(function);
574
fi
575
576
if let group: Symbols.FUNCTION_GROUP = member then
577
for f in group.functions do
578
if accepts_index(f) then
579
return true;
580
fi
581
od
582
return false;
583
fi
584
585
return false;
586
si
587
588
accepts_index(f: Symbols.Function) -> bool is
589
if !f.are_arguments_declared \/ f.arguments.count == 0 then
590
return false;
591
fi
592
593
let formal = f.arguments[0];
594
if formal.is_error then
595
return false;
596
fi
597
598
return formal.is_assignable_from(index_type);
599
si
600
601
equals(other: object?) -> bool is
602
if !other? \/ !isa INDEX_CONSTRAINT(other) then
603
return false;
604
fi
605
606
let i = other;
607
return index_type.matches(i.index_type);
608
si
609
610
get_hash_code() -> int => index_type.get_hash_code() * 31 + 29;
611
612
to_string() -> string => "index[{index_type}]";
613
si
614
si