Files
Nim/compiler/vtables.nim
Ryan McConnell 46259cd0b8 fix sortVTableDispatchers KeyError on re-entrant method registration via when isMainModule (#25856)
Encountered in realistic scenario. Didn't really look at this one. AI
one shot it lol

When a module defines method-bearing types and a when isMainModule
block imports additional modules that also define methods on the same
type hierarchy, sortVTableDispatchers crashes with:

Error: unhandled exception: key not found: (module: N, item: M)
[KeyError]

Root cause: the itemTable built during vtable sorting is populated
from g.objectTree[baseType], which only contains types from the
current compilation pass. When when isMainModule triggers re-import
of method-bearing modules, the method bucket contains types from both
passes. Types from the first pass have ItemIds not present in the
second pass's object tree, so itemTable[obj.itemId] raises KeyError
at line 155.

Fix: if obj.itemId is missing from itemTable, create an empty slot
array of the correct length. The entry is a local temporary — the
second loop in sortVTableDispatchers only calls setMethodsPerType
for types in the current object tree, so types from the prior pass
retain their already-established dispatch. The entry exists solely to
prevent the KeyError during the assignment loop.

The methodIndexLen used for the new entry is the bucket's slot count,
which is correct for any type in the hierarchy.

Added test tests/method/tvtable_reentry.nim that defines methods
across three types in two compilation passes and verifies dispatch
correctness for all three.
2026-06-05 16:37:00 +02:00

169 lines
6.6 KiB
Nim

import ast, modulegraphs, magicsys, lineinfos, options, cgmeth, types
import std/[algorithm, tables, intsets, assertions]
proc genVTableDispatcher(g: ModuleGraph; methods: seq[PSym]; index: int): PSym =
#[
proc dispatch(x: Base, params: ...) =
cast[proc bar(x: Base, params: ...)](x.vTable[index])(x, params)
]#
var base = methods[0].ast[dispatcherPos].sym
result = base
var paramLen = base.typ.signatureLen
var body = newNodeI(nkStmtList, base.info)
var disp = newNodeI(nkIfStmt, base.info)
let nimGetVTableSym = getCompilerProc(g, "nimGetVTable")
let ptrPNimType = nimGetVTableSym.typ.n[1].sym.typ
var nTyp = base.typ.n[1].sym.typ
var dispatchObject = newSymNode(base.typ.n[1].sym)
if nTyp.kind == tyObject:
dispatchObject = newTree(nkAddr, dispatchObject)
else:
if g.config.backend != backendCpp: # TODO: maybe handle ptr?
if nTyp.kind == tyVar and nTyp.skipTypes({tyVar}).kind != tyObject:
dispatchObject = newTree(nkDerefExpr, dispatchObject)
var getVTableCall = newTree(nkCall,
newSymNode(nimGetVTableSym),
dispatchObject,
newIntNode(nkIntLit, index)
)
getVTableCall.typ = getSysType(g, unknownLineInfo, tyPointer)
var vTableCall = newNodeIT(nkCall, base.info, base.typ.returnType)
var castNode = newTree(nkCast,
newNodeIT(nkType, base.info, base.typ),
getVTableCall)
castNode.typ = base.typ
vTableCall.add castNode
for col in 1..<paramLen:
let param = base.typ.n[col].sym
vTableCall.add newSymNode(param)
var ret: PNode
if base.typ.returnType != nil:
var a = newNodeI(nkFastAsgn, base.info)
a.add newSymNode(base.ast[resultPos].sym)
a.add vTableCall
ret = newNodeI(nkReturnStmt, base.info)
ret.add a
else:
ret = vTableCall
if base.typ.n[1].sym.typ.skipTypes(abstractInst).kind in {tyRef, tyPtr}:
let ifBranch = newNodeI(nkElifBranch, base.info)
let boolType = getSysType(g, unknownLineInfo, tyBool)
var isNil = getSysMagic(g, unknownLineInfo, "isNil", mIsNil)
let checkSelf = newNodeIT(nkCall, base.info, boolType)
checkSelf.add newSymNode(isNil)
checkSelf.add newSymNode(base.typ.n[1].sym)
ifBranch.add checkSelf
ifBranch.add newTree(nkCall,
newSymNode(getCompilerProc(g, "chckNilDisp")), newSymNode(base.typ.n[1].sym))
let elseBranch = newTree(nkElifBranch, ret)
disp.add ifBranch
disp.add elseBranch
else:
disp = ret
body.add disp
body.flags.incl nfTransf # should not be further transformed
result.ast[bodyPos] = body
proc containGenerics(base: PType, s: seq[tuple[depth: int, value: PType]]): bool =
result = tfHasMeta in base.flags
for i in s:
if tfHasMeta in i.value.flags:
result = true
break
proc collectVTableDispatchers*(g: ModuleGraph) =
var itemTable = initTable[ItemId, seq[PSym]]()
var rootTypeSeq = newSeq[PType]()
var rootItemIdCount = initCountTable[ItemId]()
for bucket in 0..<g.methods.len:
var relevantCols = initIntSet()
if relevantCol(g.methods[bucket].methods, 1): incl(relevantCols, 1)
sortBucket(g.methods[bucket].methods, relevantCols)
let base = g.methods[bucket].methods[^1]
let baseType = base.typ.firstParamType.skipTypes(skipPtrs-{tyTypeDesc})
if baseType.itemId in g.objectTree and not containGenerics(baseType, g.objectTree[baseType.itemId]):
let methodIndexLen = g.bucketTable[baseType.itemId]
if baseType.itemId notin itemTable: # once is enough
rootTypeSeq.add baseType
itemTable[baseType.itemId] = newSeq[PSym](methodIndexLen)
sort(g.objectTree[baseType.itemId], cmp = proc (x, y: tuple[depth: int, value: PType]): int =
if x.depth >= y.depth: 1
else: -1
)
for item in g.objectTree[baseType.itemId]:
if item.value.itemId notin itemTable:
itemTable[item.value.itemId] = newSeq[PSym](methodIndexLen)
var mIndex = 0 # here is the correpsonding index
if baseType.itemId notin rootItemIdCount:
rootItemIdCount[baseType.itemId] = 1
else:
mIndex = rootItemIdCount[baseType.itemId]
rootItemIdCount.inc(baseType.itemId)
for idx in 0..<g.methods[bucket].methods.len:
let obj = g.methods[bucket].methods[idx].typ.firstParamType.skipTypes(skipPtrs)
itemTable[obj.itemId][mIndex] = g.methods[bucket].methods[idx]
g.addDispatchers genVTableDispatcher(g, g.methods[bucket].methods, mIndex)
else: # if the base object doesn't have this method
g.addDispatchers genIfDispatcher(g, g.methods[bucket].methods, relevantCols, g.idgen)
proc sortVTableDispatchers*(g: ModuleGraph) =
var itemTable = initTable[ItemId, seq[PSym]]()
var rootTypeSeq = newSeq[ItemId]()
var rootItemIdCount = initCountTable[ItemId]()
for bucket in 0..<g.methods.len:
var relevantCols = initIntSet()
if relevantCol(g.methods[bucket].methods, 1): incl(relevantCols, 1)
sortBucket(g.methods[bucket].methods, relevantCols)
let base = g.methods[bucket].methods[^1]
let baseType = base.typ.firstParamType.skipTypes(skipPtrs-{tyTypeDesc})
if baseType.itemId in g.objectTree and not containGenerics(baseType, g.objectTree[baseType.itemId]):
let methodIndexLen = g.bucketTable[baseType.itemId]
if baseType.itemId notin itemTable: # once is enough
rootTypeSeq.add baseType.itemId
itemTable[baseType.itemId] = newSeq[PSym](methodIndexLen)
sort(g.objectTree[baseType.itemId], cmp = proc (x, y: tuple[depth: int, value: PType]): int =
if x.depth >= y.depth: 1
else: -1
)
for item in g.objectTree[baseType.itemId]:
if item.value.itemId notin itemTable:
itemTable[item.value.itemId] = newSeq[PSym](methodIndexLen)
var mIndex = 0 # here is the correpsonding index
if baseType.itemId notin rootItemIdCount:
rootItemIdCount[baseType.itemId] = 1
else:
mIndex = rootItemIdCount[baseType.itemId]
rootItemIdCount.inc(baseType.itemId)
for idx in 0..<g.methods[bucket].methods.len:
let obj = g.methods[bucket].methods[idx].typ.firstParamType.skipTypes(skipPtrs)
if obj.itemId notin itemTable:
itemTable[obj.itemId] = newSeq[PSym](methodIndexLen)
itemTable[obj.itemId][mIndex] = g.methods[bucket].methods[idx]
for baseType in rootTypeSeq:
g.setMethodsPerType(baseType, itemTable[baseType])
for item in g.objectTree[baseType]:
let typ = item.value.skipTypes(skipPtrs)
let idx = typ.itemId
for mIndex in 0..<itemTable[idx].len:
if itemTable[idx][mIndex] == nil:
let parentIndex = typ.baseClass.skipTypes(skipPtrs).itemId
itemTable[idx][mIndex] = itemTable[parentIndex][mIndex]
g.setMethodsPerType(idx, itemTable[idx])