Files
Nim/compiler/semobjconstr.nim
metagn 87c306061b disable weird type inference for object constructors (#24455)
closes #24372, refs #20091

This was added in #20091 for some reason but doesn't actually work and
only makes error messages more obscure. So for now, it's disabled.

Can also be backported to 2.0 if necessary.

(cherry picked from commit a610f23060)
2025-01-14 09:08:06 +01:00

540 lines
22 KiB
Nim

#
#
# The Nim Compiler
# (c) Copyright 2015 Nim Contributors
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## This module implements Nim's object construction rules.
# included from sem.nim
from std/sugar import dup
type
ObjConstrContext = object
typ: PType # The constructed type
initExpr: PNode # The init expression (nkObjConstr)
needsFullInit: bool # A `requiresInit` derived type will
# set this to true while visiting
# parent types.
missingFields: seq[PSym] # Fields that the user failed to specify
checkDefault: bool # Checking defaults
InitStatus = enum # This indicates the result of object construction
initUnknown
initFull # All of the fields have been initialized
initPartial # Some of the fields have been initialized
initNone # None of the fields have been initialized
initConflict # Fields from different branches have been initialized
proc semConstructFields(c: PContext, n: PNode, constrCtx: var ObjConstrContext,
flags: TExprFlags): tuple[status: InitStatus, defaults: seq[PNode]]
proc mergeInitStatus(existing: var InitStatus, newStatus: InitStatus) =
case newStatus
of initConflict:
existing = newStatus
of initPartial:
if existing in {initUnknown, initFull, initNone}:
existing = initPartial
of initNone:
if existing == initUnknown:
existing = initNone
elif existing == initFull:
existing = initPartial
of initFull:
if existing == initUnknown:
existing = initFull
elif existing == initNone:
existing = initPartial
of initUnknown:
discard
proc invalidObjConstr(c: PContext, n: PNode) =
if n.kind == nkInfix and n[0].kind == nkIdent and n[0].ident.s[0] == ':':
localError(c.config, n.info, "incorrect object construction syntax; use a space after the colon")
else:
localError(c.config, n.info, "incorrect object construction syntax")
proc locateFieldInInitExpr(c: PContext, field: PSym, initExpr: PNode): PNode =
# Returns the assignment nkExprColonExpr node or nil
result = nil
let fieldId = field.name.id
for i in 1..<initExpr.len:
let assignment = initExpr[i]
if assignment.kind != nkExprColonExpr:
invalidObjConstr(c, assignment)
elif fieldId == considerQuotedIdent(c, assignment[0]).id:
return assignment
proc semConstrField(c: PContext, flags: TExprFlags,
field: PSym, initExpr: PNode): PNode =
let assignment = locateFieldInInitExpr(c, field, initExpr)
if assignment != nil:
if nfSem in assignment.flags: return assignment[1]
if nfSkipFieldChecking in assignment[1].flags:
discard
elif not fieldVisible(c, field):
localError(c.config, initExpr.info,
"the field '$1' is not accessible." % [field.name.s])
return
var initValue = semExprFlagDispatched(c, assignment[1], flags, field.typ)
if initValue != nil:
initValue = fitNodeConsiderViewType(c, field.typ, initValue, assignment.info)
initValue.flags.incl nfSkipFieldChecking
assignment[0] = newSymNode(field)
assignment[1] = initValue
assignment.flags.incl nfSem
result = initValue
else:
result = nil
proc branchVals(c: PContext, caseNode: PNode, caseIdx: int,
isStmtBranch: bool): IntSet =
if caseNode[caseIdx].kind == nkOfBranch:
result = initIntSet()
for val in processBranchVals(caseNode[caseIdx]):
result.incl(val)
else:
result = c.getIntSetOfType(caseNode[0].typ)
for i in 1..<caseNode.len-1:
for val in processBranchVals(caseNode[i]):
result.excl(val)
proc findUsefulCaseContext(c: PContext, discrimator: PNode): (PNode, int) =
result = (nil, 0)
for i in countdown(c.p.caseContext.high, 0):
let
(caseNode, index) = c.p.caseContext[i]
skipped = caseNode[0].skipHidden
if skipped.kind == nkSym and skipped.sym == discrimator.sym:
return (caseNode, index)
proc pickCaseBranch(caseExpr, matched: PNode): PNode =
# XXX: Perhaps this proc already exists somewhere
let endsWithElse = caseExpr[^1].kind == nkElse
for i in 1..<caseExpr.len - int(endsWithElse):
if caseExpr[i].caseBranchMatchesExpr(matched):
return caseExpr[i]
if endsWithElse:
result = caseExpr[^1]
else:
result = nil
iterator directFieldsInRecList(recList: PNode): PNode =
# XXX: We can remove this case by making all nkOfBranch nodes
# regular. Currently, they try to avoid using nkRecList if they
# include only a single field
if recList.kind == nkSym:
yield recList
else:
doAssert recList.kind == nkRecList
for field in recList:
if field.kind == nkSym:
yield field
template quoteStr(s: string): string = "'" & s & "'"
proc fieldsPresentInInitExpr(c: PContext, fieldsRecList, initExpr: PNode): string =
result = ""
for field in directFieldsInRecList(fieldsRecList):
if locateFieldInInitExpr(c, field.sym, initExpr) != nil:
if result.len != 0: result.add ", "
result.add field.sym.name.s.quoteStr
proc locateFieldInDefaults(sym: PSym, defaults: seq[PNode]): bool =
result = false
for d in defaults:
if sym.id == d[0].sym.id:
return true
proc collectMissingFields(c: PContext, fieldsRecList: PNode,
constrCtx: var ObjConstrContext, defaults: seq[PNode]
): seq[PSym] =
result = @[]
for r in directFieldsInRecList(fieldsRecList):
let assignment = locateFieldInInitExpr(c, r.sym, constrCtx.initExpr)
if assignment == nil and not locateFieldInDefaults(r.sym, defaults):
if constrCtx.needsFullInit or
sfRequiresInit in r.sym.flags or
r.sym.typ.requiresInit:
constrCtx.missingFields.add r.sym
else:
result.add r.sym
proc collectMissingCaseFields(c: PContext, branchNode: PNode,
constrCtx: var ObjConstrContext, defaults: seq[PNode]): seq[PSym] =
if branchNode != nil:
let fieldsRecList = branchNode[^1]
result = collectMissingFields(c, fieldsRecList, constrCtx, defaults)
else:
result = @[]
proc collectOrAddMissingCaseFields(c: PContext, branchNode: PNode,
constrCtx: var ObjConstrContext, defaults: var seq[PNode]) =
let res = collectMissingCaseFields(c, branchNode, constrCtx, defaults)
for sym in res:
let asgnType = newType(tyTypeDesc, c.idgen, sym.typ.owner)
let recTyp = sym.typ.skipTypes(defaultFieldsSkipTypes)
rawAddSon(asgnType, recTyp)
let asgnExpr = newTree(nkCall,
newSymNode(getSysMagic(c.graph, constrCtx.initExpr.info, "zeroDefault", mZeroDefault)),
newNodeIT(nkType, constrCtx.initExpr.info, asgnType)
)
asgnExpr.flags.incl nfSkipFieldChecking
asgnExpr.typ() = recTyp
defaults.add newTree(nkExprColonExpr, newSymNode(sym), asgnExpr)
proc collectBranchFields(c: PContext, n: PNode, discriminatorVal: PNode,
constrCtx: var ObjConstrContext, flags: TExprFlags) =
# All bets are off. If any of the branches has a mandatory
# fields we must produce an error:
for i in 1..<n.len:
let branchNode = n[i]
if branchNode != nil:
let oldCheckDefault = constrCtx.checkDefault
constrCtx.checkDefault = true
let (_, defaults) = semConstructFields(c, branchNode[^1], constrCtx, flags)
constrCtx.checkDefault = oldCheckDefault
if len(defaults) > 0:
localError(c.config, discriminatorVal.info, "branch initialization " &
"with a runtime discriminator is not supported " &
"for a branch whose fields have default values.")
discard collectMissingCaseFields(c, n[i], constrCtx, @[])
proc semConstructFields(c: PContext, n: PNode, constrCtx: var ObjConstrContext,
flags: TExprFlags): tuple[status: InitStatus, defaults: seq[PNode]] =
result = (initUnknown, @[])
case n.kind
of nkRecList:
for field in n:
let (subSt, subDf) = semConstructFields(c, field, constrCtx, flags)
result.status.mergeInitStatus subSt
result.defaults.add subDf
of nkRecCase:
template fieldsPresentInBranch(branchIdx: int): string =
let branch = n[branchIdx]
let fields = branch[^1]
fieldsPresentInInitExpr(c, fields, constrCtx.initExpr)
let discriminator = n[0]
internalAssert c.config, discriminator.kind == nkSym
var selectedBranch = -1
for i in 1..<n.len:
let innerRecords = n[i][^1]
let (status, _) = semConstructFields(c, innerRecords, constrCtx, flags) # todo
if status notin {initNone, initUnknown}:
result.status.mergeInitStatus status
if selectedBranch != -1:
let prevFields = fieldsPresentInBranch(selectedBranch)
let currentFields = fieldsPresentInBranch(i)
localError(c.config, constrCtx.initExpr.info,
("The fields '$1' and '$2' cannot be initialized together, " &
"because they are from conflicting branches in the case object.") %
[prevFields, currentFields])
result.status = initConflict
else:
selectedBranch = i
if selectedBranch != -1:
template badDiscriminatorError =
if c.inUncheckedAssignSection == 0:
let fields = fieldsPresentInBranch(selectedBranch)
localError(c.config, constrCtx.initExpr.info,
("cannot prove that it's safe to initialize $1 with " &
"the runtime value for the discriminator '$2' ") %
[fields, discriminator.sym.name.s])
mergeInitStatus(result.status, initNone)
template wrongBranchError(i) =
if c.inUncheckedAssignSection == 0:
let fields = fieldsPresentInBranch(i)
localError(c.config, constrCtx.initExpr.info,
("a case selecting discriminator '$1' with value '$2' " &
"appears in the object construction, but the field(s) $3 " &
"are in conflict with this value.") %
[discriminator.sym.name.s, discriminatorVal.renderTree, fields])
template valuesInConflictError(valsDiff) =
localError(c.config, discriminatorVal.info, ("possible values " &
"$2 are in conflict with discriminator values for " &
"selected object branch $1.") % [$selectedBranch,
valsDiff.renderAsType(n[0].typ)])
let branchNode = n[selectedBranch]
let flags = {efPreferStatic, efPreferNilResult}
var discriminatorVal = semConstrField(c, flags,
discriminator.sym,
constrCtx.initExpr)
if discriminatorVal != nil:
discriminatorVal = discriminatorVal.skipHidden
if discriminatorVal.kind notin nkLiterals and (
not isOrdinalType(discriminatorVal.typ, true) or
lengthOrd(c.config, discriminatorVal.typ) > MaxSetElements or
lengthOrd(c.config, n[0].typ) > MaxSetElements):
localError(c.config, discriminatorVal.info,
"branch initialization with a runtime discriminator only " &
"supports ordinal types with 2^16 elements or less.")
if discriminatorVal == nil:
badDiscriminatorError()
elif discriminatorVal.kind == nkSym:
let (ctorCase, ctorIdx) = findUsefulCaseContext(c, discriminatorVal)
if ctorCase == nil:
if discriminatorVal.typ.kind == tyRange:
let rangeVals = c.getIntSetOfType(discriminatorVal.typ)
let recBranchVals = branchVals(c, n, selectedBranch, false)
let diff = rangeVals - recBranchVals
if diff.len != 0:
valuesInConflictError(diff)
else:
badDiscriminatorError()
elif discriminatorVal.sym.kind notin {skLet, skParam} or
discriminatorVal.sym.typ.kind in {tyVar}:
if c.inUncheckedAssignSection == 0:
localError(c.config, discriminatorVal.info,
"runtime discriminator must be immutable if branch fields are " &
"initialized, a 'let' binding is required.")
elif ctorCase[ctorIdx].kind == nkElifBranch:
localError(c.config, discriminatorVal.info, "branch initialization " &
"with a runtime discriminator is not supported inside of an " &
"`elif` branch.")
else:
var
ctorBranchVals = branchVals(c, ctorCase, ctorIdx, true)
recBranchVals = branchVals(c, n, selectedBranch, false)
branchValsDiff = ctorBranchVals - recBranchVals
if branchValsDiff.len != 0:
valuesInConflictError(branchValsDiff)
else:
var failedBranch = -1
if branchNode.kind != nkElse:
if not branchNode.caseBranchMatchesExpr(discriminatorVal):
failedBranch = selectedBranch
else:
# With an else clause, check that all other branches don't match:
for i in 1..<n.len - 1:
if n[i].caseBranchMatchesExpr(discriminatorVal):
failedBranch = i
break
if failedBranch != -1:
if discriminatorVal.typ.kind == tyRange:
let rangeVals = c.getIntSetOfType(discriminatorVal.typ)
let recBranchVals = branchVals(c, n, selectedBranch, false)
let diff = rangeVals - recBranchVals
if diff.len != 0:
valuesInConflictError(diff)
else:
wrongBranchError(failedBranch)
let (_, defaults) = semConstructFields(c, branchNode[^1], constrCtx, flags)
result.defaults.add defaults
# When a branch is selected with a partial match, some of the fields
# that were not initialized may be mandatory. We must check for this:
if result.status == initPartial:
collectOrAddMissingCaseFields(c, branchNode, constrCtx, result.defaults)
else:
result.status = initNone
let discriminatorVal = semConstrField(c, flags + {efPreferStatic},
discriminator.sym,
constrCtx.initExpr)
if discriminatorVal == nil:
if discriminator.sym.ast != nil:
# branch is selected by the default field value of discriminator
let discriminatorDefaultVal = discriminator.sym.ast
result.status = initUnknown
result.defaults.add newTree(nkExprColonExpr, n[0], discriminatorDefaultVal)
if discriminatorDefaultVal.kind == nkIntLit:
let matchedBranch = n.pickCaseBranch discriminatorDefaultVal
if matchedBranch != nil:
let (_, defaults) = semConstructFields(c, matchedBranch[^1], constrCtx, flags)
result.defaults.add defaults
collectOrAddMissingCaseFields(c, matchedBranch, constrCtx, result.defaults)
else:
collectBranchFields(c, n, discriminatorDefaultVal, constrCtx, flags)
else:
# None of the branches were explicitly selected by the user and no
# value was given to the discrimator. We can assume that it will be
# initialized to zero and this will select a particular branch as
# a result:
let defaultValue = newIntLit(c.graph, constrCtx.initExpr.info, 0)
let matchedBranch = n.pickCaseBranch defaultValue
discard collectMissingCaseFields(c, matchedBranch, constrCtx, @[])
else:
result.status = initPartial
if discriminatorVal.kind == nkIntLit:
# When the discriminator is a compile-time value, we also know
# which branch will be selected:
let matchedBranch = n.pickCaseBranch discriminatorVal
if matchedBranch != nil:
let (_, defaults) = semConstructFields(c, matchedBranch[^1], constrCtx, flags)
result.defaults.add defaults
collectOrAddMissingCaseFields(c, matchedBranch, constrCtx, result.defaults)
else:
collectBranchFields(c, n, discriminatorVal, constrCtx, flags)
of nkSym:
let field = n.sym
let e = semConstrField(c, flags, field, constrCtx.initExpr)
if e != nil:
result.status = initFull
elif field.ast != nil:
if efIgnoreDefaults notin flags:
result.status = initUnknown
result.defaults.add newTree(nkExprColonExpr, n, field.ast)
else:
result.status = initNone
else:
if {efWantNoDefaults, efIgnoreDefaults} * flags == {}: # cannot compute defaults at the typeRightPass
let defaultExpr = defaultNodeField(c, n, constrCtx.checkDefault)
if defaultExpr != nil:
result.status = initUnknown
result.defaults.add newTree(nkExprColonExpr, n, defaultExpr)
else:
result.status = initNone
else:
result.status = initNone
else:
internalAssert c.config, false
proc semConstructTypeAux(c: PContext,
constrCtx: var ObjConstrContext,
flags: TExprFlags): tuple[status: InitStatus, defaults: seq[PNode]] =
result = (initUnknown, @[])
var t = constrCtx.typ
while true:
let (status, defaults) = semConstructFields(c, t.n, constrCtx, flags)
result.status.mergeInitStatus status
result.defaults.add defaults
if status in {initPartial, initNone, initUnknown}:
discard collectMissingFields(c, t.n, constrCtx, result.defaults)
let base = t.baseClass
if base == nil or base.id == t.id or
base.kind in {tyRef, tyPtr} and base.elementType.id == t.id:
break
t = skipTypes(base, skipPtrs)
if t.kind != tyObject:
# XXX: This is not supposed to happen, but apparently
# there are some issues in semtypinst. Luckily, it
# seems to affect only `computeRequiresInit`.
return
constrCtx.needsFullInit = constrCtx.needsFullInit or
tfNeedsFullInit in t.flags
proc initConstrContext(t: PType, initExpr: PNode): ObjConstrContext =
ObjConstrContext(typ: t, initExpr: initExpr,
needsFullInit: tfNeedsFullInit in t.flags)
proc computeRequiresInit(c: PContext, t: PType): bool =
assert t.kind == tyObject
var constrCtx = initConstrContext(t, newNode(nkObjConstr))
let initResult = semConstructTypeAux(c, constrCtx, {efWantNoDefaults})
constrCtx.missingFields.len > 0
proc defaultConstructionError(c: PContext, t: PType, info: TLineInfo) =
var objType = t
while objType.kind notin {tyObject, tyDistinct}:
objType = objType.last
assert objType != nil
if objType.kind == tyObject:
var constrCtx = initConstrContext(objType, newNodeI(nkObjConstr, info))
let initResult = semConstructTypeAux(c, constrCtx, {efIgnoreDefaults})
if constrCtx.missingFields.len > 0:
localError(c.config, info,
"The $1 type doesn't have a default value. The following fields must be initialized: $2." % [typeToString(t), listSymbolNames(constrCtx.missingFields)])
elif objType.kind == tyDistinct:
localError(c.config, info,
"The $1 distinct type doesn't have a default value." % typeToString(t))
else:
assert false, "Must not enter here."
proc semObjConstr(c: PContext, n: PNode, flags: TExprFlags; expectedType: PType = nil): PNode =
var t = semTypeNode(c, n[0], nil)
result = newNodeIT(nkObjConstr, n.info, t)
for i in 0..<n.len:
result.add n[i]
if t == nil:
return localErrorNode(c, result, "object constructor needs an object type")
when false:
# attempted type inference for generic object types,
# doesn't work since n[0] isn't set and seems underspecified
if t.skipTypes({tyGenericInst,
tyAlias, tySink, tyOwned, tyRef}).kind != tyObject and
expectedType != nil and expectedType.skipTypes({tyGenericInst,
tyAlias, tySink, tyOwned, tyRef}).kind == tyObject:
t = expectedType
t = skipTypes(t, {tyGenericInst, tyAlias, tySink, tyOwned})
if t.kind == tyRef:
t = skipTypes(t.elementType, {tyGenericInst, tyAlias, tySink, tyOwned})
if optOwnedRefs in c.config.globalOptions:
result.typ() = makeVarType(c, result.typ, tyOwned)
# we have to watch out, there are also 'owned proc' types that can be used
# multiple times as long as they don't have closures.
result.typ.flags.incl tfHasOwned
if t.kind != tyObject:
return localErrorNode(c, result, if t.kind != tyGenericBody:
"object constructor needs an object type".dup(addTypeNodeDeclaredLoc(c.config, t))
else: "cannot instantiate: '" &
typeToString(t, preferDesc) &
"'; the object's generic parameters cannot be inferred and must be explicitly given"
)
# Check if the object is fully initialized by recursively testing each
# field (if this is a case object, initialized fields in two different
# branches will be reported as an error):
var constrCtx = initConstrContext(t, result)
let (initResult, defaults) = semConstructTypeAux(c, constrCtx, flags)
var hasError = false # needed to split error detect/report for better msgs
# It's possible that the object was not fully initialized while
# specifying a .requiresInit. pragma:
if constrCtx.missingFields.len > 0:
hasError = true
localError(c.config, result.info,
"The $1 type requires the following fields to be initialized: $2." %
[t.sym.name.s, listSymbolNames(constrCtx.missingFields)])
# Since we were traversing the object fields, it's possible that
# not all of the fields specified in the constructor was visited.
# We'll check for such fields here:
for i in 1..<result.len:
let field = result[i]
if nfSem notin field.flags:
if field.kind != nkExprColonExpr:
invalidObjConstr(c, field)
hasError = true
continue
let id = considerQuotedIdent(c, field[0])
# This node was not processed. There are two possible reasons:
# 1) It was shadowed by a field with the same name on the left
for j in 1..<i:
let prevId = considerQuotedIdent(c, result[j][0])
if prevId.id == id.id:
localError(c.config, field.info, errFieldInitTwice % id.s)
hasError = true
break
# 2) No such field exists in the constructed type
let msg = errUndeclaredField % id.s & " for type " & getProcHeader(c.config, t.sym)
localError(c.config, field.info, msg)
hasError = true
break
result.sons.add defaults
if initResult == initFull:
incl result.flags, nfAllFieldsSet
# wrap in an error see #17437
if hasError: result = errorNode(c, result)