concepts: fix generic concept devaluation skipped in typeRel (#26164)

Fix and tests for issues #26147 case A.
See individual commit messages.

Checked on the full test suite: no new failure with respect to devel as
baseline.
This commit is contained in:
pacien
2026-09-10 08:49:43 +02:00
committed by GitHub
parent 792e21dd9e
commit bd32dab561
3 changed files with 69 additions and 3 deletions

View File

@@ -1805,7 +1805,11 @@ proc typeRel(c: var TCandidate, f, aOrig: PType,
let inst = prepareMetatypeForSigmatch(c.c, c.bindings, c.call.info, f)
return typeRel(c, inst, a, flags)
if x.kind == tyGenericInvocation:
if concpt.kind == tyConcept:
# Concept dispatch devaluates generic concepts,
# so it must precede the generic invocation case
result = enterConceptMatch(c, f, x, flags)
elif x.kind == tyGenericInvocation:
if f[0] == x[0]:
for i in 1..<f.len:
# Handle when checking against a generic that isn't fully instantiated
@@ -1826,8 +1830,6 @@ proc typeRel(c: var TCandidate, f, aOrig: PType,
# Workaround for regression #4589
if f[i].kind != tyTypeDesc: return
result = isGeneric
elif concpt.kind == tyConcept:
result = enterConceptMatch(c, f, x, flags)
else:
let genericBody = f[0]
var askip = skippedNone

View File

@@ -73,3 +73,21 @@ proc unpackBoth[K1, V1, K2, V2;
let otherPair = Pair[string, float](k: "eight", v: 8.0)
doAssert unpackBoth(pair, otherPair) == ((7, "seven"), ("eight", 8.0))
# A concrete `items` overload must win over one declared over a concept that
# the type happens to satisfy, and not generate an "ambiguous call".
type OverloadDummy[T] = ref object
data: seq[T]
proc `[]`[T](d: OverloadDummy[T], i: int): T = d.data[i]
proc len[T](d: OverloadDummy[T]): int = d.data.len
iterator items[T](d: OverloadDummy[T]): T =
# deliberately distinguishable from the concept-provided iterator
for i in 0 ..< d.len:
yield d.data[i] * 10
var overloadAcc: seq[int] = @[]
for x in OverloadDummy[int](data: @[1, 2, 3]):
overloadAcc.add x
doAssert overloadAcc == @[10, 20, 30]

View File

@@ -0,0 +1,46 @@
discard """
action: run
"""
# The choice between a routine declared over a concept and a concrete overload
# must not depend on how many requirements the concept happens to declare.
#
# `sumGeneric` ranks concepts by requirement count, and concrete types by
# structural nesting depth, which makes sense within each category but not
# between one another. So we must make sure we don't fall through that case
# accidentally picking ambiguous calls arbitrarily (see GitHub issue #26147).
type
Small[T] = concept
proc `[]`(a: Self; index: int): T
proc len(a: Self): int
Large[T] = concept
proc `[]`(a: Self; index: int): T
proc len(a: Self): int
proc extra1(a: Self): int
proc extra2(a: Self): int
proc extra3(a: Self): int
Box[T] = ref object
data: seq[T]
proc `[]`[T](b: Box[T], i: int): T = b.data[i]
proc len[T](b: Box[T]): int = b.data.len
proc extra1[T](b: Box[T]): int = 0
proc extra2[T](b: Box[T]): int = 0
proc extra3[T](b: Box[T]): int = 0
proc pickSmall[T](x: Small[T]): string = "concept"
proc pickSmall[T](x: Box[T]): string = "concrete"
proc pickLarge[T](x: Large[T]): string = "concept"
proc pickLarge[T](x: Box[T]): string = "concrete"
let b = Box[int](data: @[1, 2, 3])
# `Box` satisfies both concepts, so both calls must select the concrete
# overload, and must agree with each other.
doAssert pickSmall(b) == "concrete"
doAssert pickLarge(b) == "concrete"
doAssert pickSmall(b) == pickLarge(b)