stricter set type match, implicit conversion for literals (#24176)

fixes #18396, fixes #20142

Set types with base types matching less than a generic match (so
subrange matches, conversion matches, int conversion matches) are now
considered mismatching, as their representation is different on the
backends (except VM and JS), causing codegen issues. An exception is
granted for set literal types, which now implicitly convert each element
to the matched base type, so things like `s == {'a', 'b'}` are still
possible where `s` is `set[range['a'..'z']]`. Also every conversion
match in this case is unified under the normal "conversion" match, so a
literal doesn't match one set type better than the other, unless it's
equal.

However `{'a', 'b'} == s` or `{'a', 'b'} - s` etc is now not possible.
when it used to work in the VM. So this is somewhat breaking, and needs
a changelog entry.
This commit is contained in:
metagn
2024-10-03 21:39:55 +03:00
committed by GitHub
parent 4eed341ba5
commit 7dfadb8b4e
5 changed files with 129 additions and 9 deletions

View File

@@ -268,6 +268,7 @@ const
NotesVerbosity* = computeNotesVerbosity()
errXMustBeCompileTime* = "'$1' can only be used in compile-time context"
errArgsNeedRunOption* = "arguments can only be given if the '--run' option is selected"
errFloatToString* = "cannot convert '$1' to '$2'"
type
TFileInfo* = object

View File

@@ -497,7 +497,6 @@ proc semAfterMacroCall(c: PContext, call, macroResult: PNode,
const
errMissingGenericParamsForTemplate = "'$1' has unspecified generic parameters"
errFloatToString = "cannot convert '$1' to '$2'"
proc semMacroExpr(c: PContext, n, nOrig: PNode, sym: PSym,
flags: TExprFlags = {}; expectedType: PType = nil): PNode =

View File

@@ -1561,11 +1561,12 @@ proc typeRel(c: var TCandidate, f, aOrig: PType,
else:
result = typeRel(c, f[0], a[0], flags)
if result < isGeneric:
if result <= isConvertible:
result = isNone
elif tfIsConstructor notin a.flags:
# set constructors are a bit special...
if tfIsConstructor notin a.flags:
# set['a'..'z'] and set[char] have different representations
result = isNone
else:
# but we can convert individual elements of the constructor
result = isConvertible
of tyPtr, tyRef:
a = reduceToBase(a)
if a.kind == f.kind:
@@ -2183,6 +2184,81 @@ proc implicitConv(kind: TNodeKind, f: PType, arg: PNode, m: TCandidate,
else:
result.add arg
proc convertLiteral(kind: TNodeKind, c: PContext, m: TCandidate; n: PNode, newType: PType): PNode =
# based off changeType but generates implicit conversions instead
template addConsiderNil(s, node) =
let val = node
if val.isNil: return nil
s.add(val)
case n.kind
of nkCurly:
result = copyNode(n)
for i in 0..<n.len:
if n[i].kind == nkRange:
var x = copyNode(n[i])
x.addConsiderNil convertLiteral(kind, c, m, n[i][0], elemType(newType))
x.addConsiderNil convertLiteral(kind, c, m, n[i][1], elemType(newType))
result.add x
else:
result.addConsiderNil convertLiteral(kind, c, m, n[i], elemType(newType))
result.typ = newType
return
of nkBracket:
result = copyNode(n)
for i in 0..<n.len:
result.addConsiderNil convertLiteral(kind, c, m, n[i], elemType(newType))
result.typ = newType
return
of nkPar, nkTupleConstr:
let tup = newType.skipTypes({tyGenericInst, tyAlias, tySink, tyDistinct})
if tup.kind == tyTuple:
result = copyNode(n)
if n.len > 0 and n[0].kind == nkExprColonExpr:
# named tuple?
for i in 0..<n.len:
var name = n[i][0]
if name.kind != nkSym:
#globalError(c.config, name.info, "invalid tuple constructor")
return nil
if tup.n != nil:
var f = getSymFromList(tup.n, name.sym.name)
if f == nil:
#globalError(c.config, name.info, "unknown identifier: " & name.sym.name.s)
return nil
result.addConsiderNil convertLiteral(kind, c, m, n[i][1], f.typ)
else:
result.addConsiderNil convertLiteral(kind, c, m, n[i][1], tup[i])
else:
for i in 0..<n.len:
result.addConsiderNil convertLiteral(kind, c, m, n[i], tup[i])
result.typ = newType
return
of nkCharLit..nkUInt64Lit:
if n.kind != nkUInt64Lit and not sameTypeOrNil(n.typ, newType) and isOrdinalType(newType):
let value = n.intVal
if value < firstOrd(c.config, newType) or value > lastOrd(c.config, newType):
return nil
result = copyNode(n)
result.typ = newType
return
of nkFloatLit..nkFloat64Lit:
if newType.skipTypes(abstractVarRange-{tyTypeDesc}).kind == tyFloat:
if not floatRangeCheck(n.floatVal, newType):
return nil
result = copyNode(n)
result.typ = newType
return
of nkSym:
if n.sym.kind == skEnumField and not sameTypeOrNil(n.sym.typ, newType) and isOrdinalType(newType):
let value = n.sym.position
if value < firstOrd(c.config, newType) or value > lastOrd(c.config, newType):
return nil
result = copyNode(n)
result.typ = newType
return
else: discard
return implicitConv(kind, newType, n, m, c)
proc isLValue(c: PContext; n: PNode, isOutParam = false): bool {.inline.} =
let aa = isAssignable(nil, n)
case aa
@@ -2394,7 +2470,20 @@ proc paramTypesMatchAux(m: var TCandidate, f, a: PType,
if f.skipTypes({tyRange}).kind in {tyInt, tyUInt}:
inc(m.convMatches)
inc(m.convMatches)
result = implicitConv(nkHiddenStdConv, f, arg, m, c)
if skipTypes(f, abstractVar-{tyTypeDesc}).kind == tySet:
if tfIsConstructor in a.flags and arg.kind == nkCurly:
# we marked the set as convertible only because the arg is a literal
# in which case we individually convert each element
let t =
if containsGenericType(f):
getInstantiatedType(c, arg, m, f).skipTypes({tySink})
else:
f.skipTypes({tySink})
result = convertLiteral(nkHiddenStdConv, c, m, arg, t)
else:
result = nil
else:
result = implicitConv(nkHiddenStdConv, f, arg, m, c)
of isIntConv:
# I'm too lazy to introduce another ``*matches`` field, so we conflate
# ``isIntConv`` and ``isIntLit`` here:

View File

@@ -0,0 +1,32 @@
block: # issue #20142
let
s1: set['a' .. 'g'] = {'a', 'e'}
s2: set['a' .. 'g'] = {'b', 'c', 'd', 'f'} # this works fine
s3 = {'b', 'c', 'd', 'f'}
doAssert s1 != s2
doAssert s1 == {range['a'..'g'] 'a', 'e'}
doAssert s2 == {range['a'..'g'] 'b', 'c', 'd', 'f'}
# literal conversion:
doAssert s1 == {'a', 'e'}
doAssert s2 == {'b', 'c', 'd', 'f'}
doAssert s3 == {'b', 'c', 'd', 'f'}
doAssert not compiles(s1 == s3)
doAssert not compiles(s2 == s3)
# can't convert literal 'z', overload match fails
doAssert not compiles(s1 == {'a', 'z'})
block: # issue #18396
var s1: set[char] = {'a', 'b'}
var s2: set['a'..'z'] = {'a', 'b'}
doAssert s1 == {'a', 'b'}
doAssert s2 == {range['a'..'z'] 'a', 'b'}
doAssert s2 == {'a', 'b'}
doAssert not compiles(s1 == s2)
block: # issue #16270
var s1: set[char] = {'a', 'b'}
var s2: set['a'..'z'] = {'a', 'c'}
doAssert not (compiles do: s2 = s2 + s1)
s2 = s2 + {'a', 'b'}
doAssert s2 == {'a', 'b', 'c'}

View File

@@ -45,7 +45,6 @@ block:
TNoteKind = range[k1..k2]
var notes: set[TNoteKind]
notes = {k0} #[tt.Error
^ cannot convert 'k0' to 'TNoteKind = range 1..2(TMsgKind)]#
^ type mismatch: got <set[TMsgKind]> but expected 'set[TNoteKind]']#
notes = {k0..k3} #[tt.Error
^ cannot convert 'k0' to 'TNoteKind = range 1..2(TMsgKind)'; tt.Error
^ cannot convert 'k3' to 'TNoteKind = range 1..2(TMsgKind)']#
^ type mismatch: got <set[TMsgKind]> but expected 'set[TNoteKind]']#