Merge pull request #7227 from kalsprite/poly

Improve procedure group overload scoring
This commit is contained in:
gingerBill
2026-08-08 12:08:28 +02:00
committed by GitHub
2 changed files with 214 additions and 16 deletions

View File

@@ -735,35 +735,38 @@ gb_internal i64 check_distance_between_types(CheckerContext *c, Operand *operand
if (operand->mode == Addressing_Constant) {
if (check_representable_as_constant(c, operand->value, dst, nullptr)) {
if (is_type_typed(dst) && src->kind == Type_Basic) {
// NOTE: prefer the untyped constant's default type (int, f64, string, bool,
// rune) over other members of the same family, so a call like g(1) picks
// `int` over `i64` instead of scoring them identically and going ambiguous.
switch (src->Basic.kind) {
case Basic_UntypedBool:
if (is_type_boolean(dst)) {
return 1;
return are_types_identical(dst, default_type(src)) ? 1 : 2;
}
break;
case Basic_UntypedRune:
if (is_type_integer(dst) || is_type_rune(dst)) {
return 1;
return are_types_identical(dst, default_type(src)) ? 1 : 2;
}
break;
case Basic_UntypedInteger:
if (is_type_integer(dst) || is_type_rune(dst)) {
return 1;
return are_types_identical(dst, default_type(src)) ? 1 : 2;
}
break;
case Basic_UntypedString:
if (is_type_string(dst)) {
return 1;
return are_types_identical(dst, default_type(src)) ? 1 : 2;
}
break;
case Basic_UntypedFloat:
if (is_type_float(dst)) {
return 1;
return are_types_identical(dst, default_type(src)) ? 1 : 2;
}
break;
case Basic_UntypedComplex:
if (is_type_complex(dst)) {
return 1;
return are_types_identical(dst, default_type(src)) ? 1 : 2;
}
if (is_type_quaternion(dst)) {
return 2;
@@ -771,12 +774,13 @@ gb_internal i64 check_distance_between_types(CheckerContext *c, Operand *operand
break;
case Basic_UntypedQuaternion:
if (is_type_quaternion(dst)) {
return 1;
return are_types_identical(dst, default_type(src)) ? 1 : 2;
}
break;
}
}
return 2;
// A cross-family constant conversion ranks below a same-family one.
return 3;
}
return -1;
}
@@ -7008,6 +7012,11 @@ gb_internal CallArgumentError check_call_arguments_internal(CheckerContext *c, A
error(o->expr, "'..' in a variadic procedure can only have one variadic argument at the end");
}
if (data) {
// A synthesised default argument is not evidence of a better match: it
// contributes assign_score_function(1) as a dummy bonus and is then scored
// again as a perfect-match argument. Discount both, plus 1 to break the
// resulting tie, so an exact-arity overload wins.
score -= dummy_argument_count * (assign_score_function(0) + assign_score_function(1) + 1);
data->score = score;
data->result_type = final_proc_type->Proc.results;
data->gen_entity = gen_entity;
@@ -7037,6 +7046,11 @@ gb_internal CallArgumentError check_call_arguments_internal(CheckerContext *c, A
}
if (data) {
// A synthesised default argument is not evidence of a better match: it
// contributes assign_score_function(1) as a dummy bonus and is then scored
// again as a perfect-match argument. Discount both, plus 1 to break the
// resulting tie, so an exact-arity overload wins.
score -= dummy_argument_count * (assign_score_function(0) + assign_score_function(1) + 1);
data->score = score;
data->result_type = final_proc_type->Proc.results;
data->gen_entity = gen_entity;
@@ -7589,8 +7603,37 @@ gb_internal CallArgumentData check_call_arguments_proc_group(CheckerContext *c,
array_add(&proc_entities, data.gen_entity);
index = proc_entities.count-1;
// prefer non-polymorphic procedures over polymorphic
item.score += assign_score_function(1);
// Order candidates:
// value-polymorphic > concrete > specialized generic > unconstrained generic
//
// `proc($S: string)` specialises on a compile-time *value*
// `proc(x: $T)` specialises on a *type* and is a fallback, so it should lose
// to an exact concrete overload
// `proc(x: $T/[]$E)` constrains that type, so it is the closer of the two
//
// These are small tie-breaks on purpose: assign_score_function(1) is
// ~a full perfect-match unit and would swamp argument match quality.
bool has_polymorphic_constant = false;
bool has_specialized_generic = false;
if (pt->Proc.params != nullptr) {
for (Entity *param : pt->Proc.params->Tuple.variables) {
if (param == nullptr) {
continue;
}
if (param->kind == Entity_Constant) {
has_polymorphic_constant = true;
}
Type *bt = base_type(param->type);
if (bt != nullptr && bt->kind == Type_Generic && bt->Generic.specialized != nullptr) {
has_specialized_generic = true;
}
}
}
if (has_polymorphic_constant) {
item.score += 2;
} else {
item.score += has_specialized_generic ? -1 : -2;
}
}
max_matched_features = gb_max(max_matched_features, matched_target_features(&pt->Proc));

View File

@@ -34,15 +34,170 @@ test_type_inference_on_literals_with_default_args :: proc(t: ^testing.T) {
}
{
// NOTE: the overloads differ in arity so that only one is ever viable, i.e. this
// checks what was inferred rather than which overload won. See
// test_proc_group_default_arg_precedence for the latter.
Bit_Set :: bit_set[enum{A, B, C}]
proc_1 :: proc(a: Bit_Set={.A}) -> Bit_Set { return a }
proc_2 :: proc(a: Bit_Set={.B}, b: Bit_Set={.C}) -> int { return 2 }
group :: proc{proc_1, proc_2}
proc_2 :: proc(a, b: Bit_Set) -> Bit_Set { return b }
group :: proc{proc_1, proc_2}
testing.expect_value(t, group(), Bit_Set{.A})
testing.expect_value(t, group(Bit_Set{.A}), 2)
testing.expect_value(t, group({.A}), 2)
testing.expect_value(t, group({.B}, {.C}), 2)
}
testing.expect_value(t, group(Bit_Set{.B}), Bit_Set{.B})
testing.expect_value(t, group({.B}), Bit_Set{.B})
testing.expect_value(t, group({.B}, {.C}), Bit_Set{.C})
}
}
@test
test_proc_group_default_arg_precedence :: proc(t: ^testing.T) {
// An overload that needs fewer default arguments synthesised is the closer match, so
// proc_1 wins whenever both are viable.
Bit_Set :: bit_set[enum{A, B, C}]
proc_1 :: proc(a: Bit_Set={.A}) -> int { return 1 }
proc_2 :: proc(a: Bit_Set={.B}, b: Bit_Set={.C}) -> int { return 2 }
group :: proc{proc_1, proc_2}
testing.expect_value(t, group(), 1) // proc_1 synthesises 1, proc_2 synthesises 2
testing.expect_value(t, group(Bit_Set{.A}), 1) // proc_1 synthesises 0, proc_2 synthesises 1
testing.expect_value(t, group({.A}), 1)
testing.expect_value(t, group({.B}, {.C}), 2) // only proc_2 takes two arguments
}
@test
test_proc_group_arity_precedence :: proc(t: ^testing.T) {
{
// a non-variadic overload is the closer match when the variadic part is empty
proc_exact :: proc(x: int) -> int { return 1 }
proc_variadic :: proc(x: int, r: ..int) -> int { return 2 }
group :: proc{proc_exact, proc_variadic}
testing.expect_value(t, group(1), 1)
testing.expect_value(t, group(1, 2, 3), 2)
}
{
// adding a defaulted sibling must not steal an exact match from an existing member
proc_int :: proc(x: int) -> int { return 1 }
proc_string :: proc(x: string) -> int { return 2 }
proc_f32 :: proc(x: f32) -> int { return 3 }
proc_f32_d :: proc(x: f32, y: int=0) -> int { return 4 }
proc_rune :: proc(x: rune) -> int { return 5 }
group :: proc{proc_int, proc_string, proc_f32, proc_f32_d, proc_rune}
v: f32
testing.expect_value(t, group(v), 3)
}
}
@test
test_proc_group_untyped_constant_default_type :: proc(t: ^testing.T) {
// An untyped constant prefers its default type over other members of the same family,
// and any same-family type over a cross-family one.
{
proc_int :: proc(int) -> int { return 1 }
proc_i64 :: proc(i64) -> int { return 2 }
group :: proc{proc_int, proc_i64}
testing.expect_value(t, group(1), 1)
}
{
// neither is the default type, but the integer family still beats the float one
proc_i64 :: proc(i64) -> int { return 1 }
proc_f64 :: proc(f64) -> int { return 2 }
group :: proc{proc_i64, proc_f64}
testing.expect_value(t, group(1), 1)
}
{
proc_f32 :: proc(f32) -> int { return 1 }
proc_f64 :: proc(f64) -> int { return 2 }
group :: proc{proc_f32, proc_f64}
testing.expect_value(t, group(1.5), 2)
}
{
proc_rune :: proc(rune) -> int { return 1 }
proc_int :: proc(int) -> int { return 2 }
group :: proc{proc_rune, proc_int}
testing.expect_value(t, group('x'), 1)
}
{
proc_string :: proc(string) -> int { return 1 }
proc_cstring :: proc(cstring) -> int { return 2 }
group :: proc{proc_string, proc_cstring}
testing.expect_value(t, group("hi"), 1)
}
{
proc_bool :: proc(bool) -> int { return 1 }
proc_b32 :: proc(b32) -> int { return 2 }
group :: proc{proc_bool, proc_b32}
testing.expect_value(t, group(true), 1)
}
{
// a value that does not fit the default type selects the overload that can hold it
proc_u8 :: proc(u8) -> int { return 1 }
proc_i64 :: proc(i64) -> int { return 2 }
group :: proc{proc_u8, proc_i64}
testing.expect_value(t, group(100000), 2)
}
}
@test
test_proc_group_polymorphic_precedence :: proc(t: ^testing.T) {
// Candidates are ordered value-polymorphic > concrete > type-polymorphic: `proc($S: T)`
// specializes on a compile-time value and is the most specific, `proc(x: $T)`
// specializes on a type and is a fallback.
{
proc_concrete :: proc(x: int) -> int { return 1 }
proc_generic :: proc(x: $T) -> int { return 2 }
group :: proc{proc_concrete, proc_generic}
testing.expect_value(t, group(1), 1)
v: int = 1
testing.expect_value(t, group(v), 1)
}
{
// the generic is the only viable overload
proc_concrete :: proc(x: string) -> int { return 1 }
proc_generic :: proc(x: $T) -> int { return 2 }
group :: proc{proc_concrete, proc_generic}
testing.expect_value(t, group(1), 2)
}
{
proc_static :: proc($S: string) -> int { return 1 }
proc_dynamic :: proc(s: string) -> int { return 2 }
group :: proc{proc_static, proc_dynamic}
testing.expect_value(t, group("literal"), 1)
s := "runtime"
testing.expect_value(t, group(s), 2) // not a constant, only proc_dynamic is viable
}
{
// all three tiers present, and the result must not depend on declaration order
proc_generic :: proc(x: $T) -> int { return 3 }
proc_concrete :: proc(s: string) -> int { return 2 }
proc_static :: proc($S: string) -> int { return 1 }
group :: proc{proc_generic, proc_concrete, proc_static}
testing.expect_value(t, group("literal"), 1)
}
// a specialised generic beats an unconstrained one
{
proc_slice :: proc(x: $T/[]$E) -> int { return 1 }
proc_generic :: proc(x: $T) -> int { return 2 }
group :: proc{proc_slice, proc_generic}
s := []int{1}
testing.expect_value(t, group(s), 1)
}
// passing a polymorphic procedure to a group whose members take procedure-typed
// parameters: only foo_concrete can accept f_poly once instantiated
{
f_poly :: proc(x: $T) -> T { return x }
foo_concrete :: proc(x: int, g: proc(int) -> int) -> int { return 1 }
foo_impossible :: proc(x: int, g: proc(int, int) -> string) -> int { return 2 }
group :: proc{foo_concrete, foo_impossible}
testing.expect_value(t, group(1, f_poly), 1)
}
}
@test