diff --git a/compiler/ast2nif.nim b/compiler/ast2nif.nim index 861cca6526..bb33ad5dbb 100644 --- a/compiler/ast2nif.nim +++ b/compiler/ast2nif.nim @@ -284,7 +284,14 @@ proc writeTypeDef(w: var Writer; dest: var TokenBuf; typ: PType) = proc writeType(w: var Writer; dest: var TokenBuf; typ: PType) = if typ == nil: dest.addDotToken() - elif typ.itemId.module == w.currentModule and typ.state == Complete: + elif typ.uniqueId.module == w.currentModule and typ.state == Complete: + # Ownership for serialization is decided by `uniqueId`, not `itemId`: the NIF + # name (`typeToNifSym`) and the loader (`createTypeStub`) both key off + # `uniqueId`, so the module that *created* the type (uniqueId.module) must be + # the one that emits its definition. `itemId.module` can be reassigned and + # diverge from `uniqueId.module`; gating on it filed the def in the wrong + # module (or nowhere), leaving dangling references (e.g. `symbol has no + # offset` for a `pointer` type whose itemId.module drifted away). typ.state = Sealed writeTypeDef(w, dest, typ) else: diff --git a/compiler/modulegraphs.nim b/compiler/modulegraphs.nim index a50d40b5e6..fe01fa6b2d 100644 --- a/compiler/modulegraphs.nim +++ b/compiler/modulegraphs.nim @@ -290,9 +290,22 @@ proc setAttachedOp*(g: ModuleGraph; module: int; t: PType; op: TTypeAttachedOp; # Use key-based deduplication for opsLog because different type objects # (e.g. canon vs orig) can have different itemIds but same structural key if key notin g.loadedOps[op]: - # Hooks should be written to the module where the type is defined, - # not the module that triggered the registration - let ownerModule = if t.sym != nil: t.sym.itemId.module.int else: module + # For a *nominal*, non-instantiated type the hook belongs to the module + # that defines the type (so it is emitted once there). But for a generic + # instance or a structural type (ref/ptr/seq/... over an instance) there is + # no honest definition site: the generic's home module is upstream of the + # type arguments and structurally blind to the instance, so it can never + # realize the hook. Such hooks can only be produced by the *instantiating* + # module — which is exactly the one running now (`module`). Stamping them + # with the type's def module produced a `LogEntry` that no module ever + # writes (the def module never instantiates it; the instantiating module's + # writer skips it because `op.module != thisModule`), so the hook was lost + # and codegen failed with "'=destroy' operator not found". Duplicate + # registrations across instantiating modules are reconciled deterministically + # at load time (see the HookEntry replay in `replayStateChanges`). + let nominal = t.sym != nil and t.kind in {tyObject, tyEnum, tyDistinct} and + tfFromGeneric notin t.flags + let ownerModule = if nominal: t.sym.itemId.module.int else: module g.opsLog.add LogEntry(kind: HookEntry, op: op, module: ownerModule, key: key, sym: value) g.loadedOps[op][key] = value g.attachedOps[op][t.itemId] = value @@ -691,7 +704,16 @@ when not defined(nimKochBootstrap): for x in result.logOps: case x.kind of HookEntry: - g.loadedOps[x.op][x.key] = x.sym + # The same structural hook may be serialized by several instantiating + # modules (a generic/structural instance has no single def site, so each + # using module owns its copy). Pick one deterministic program-wide winner + # by the smaller owning-module name, so every lookup resolves to the same + # sym regardless of module load order. + let existing = g.loadedOps[x.op].getOrDefault(x.key) + if existing == nil or + cachedModuleSuffix(g.config, x.sym.itemId.module.FileIndex) < + cachedModuleSuffix(g.config, existing.itemId.module.FileIndex): + g.loadedOps[x.op][x.key] = x.sym of ConverterEntry: g.ifaces[fileIdx.int].converters.add x.sym of MethodEntry: diff --git a/compiler/semtypinst.nim b/compiler/semtypinst.nim index ce026eac89..621ef14a1a 100644 --- a/compiler/semtypinst.nim +++ b/compiler/semtypinst.nim @@ -446,6 +446,12 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType = header[i] = x propagateToOwner(header, x) else: + # Honor the same copy-before-mutate invariant as the branch above: never + # mutate the original invocation type `t` in place. Besides being cleaner, + # under IC `t` may be a loaded dep type (Sealed/immutable), and mutating it + # would assert. The flags propagated here end up on `header`, which is what + # is used downstream (`result.flags = header.flags`). + if header == t: header = instCopyType(cl, t) propagateToOwner(header, x) if header != t: diff --git a/compiler/typekeys.nim b/compiler/typekeys.nim index d1c77ec3fe..bf9108c784 100644 --- a/compiler/typekeys.nim +++ b/compiler/typekeys.nim @@ -147,7 +147,14 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef else: symKey(c, t.symImpl, conf) of tyGenericInst: - if sfInfixCall in t.sonsImpl[0].symImpl.flagsImpl: + # The generic head (son[0]) may be a lazily-loaded stub under IC; ensure it + # is materialised before peeking at its symbol. A nil sym means this is not + # an imported C++ generic, so fall through to the normal `skipModifierB`. + var base = t.sonsImpl[0] + if base.state == Partial: + assert c.tl != nil + c.tl(base) + if base.symImpl != nil and sfInfixCall in base.symImpl.flagsImpl: # This is an imported C++ generic type. # We cannot trust the `lastSon` to hold a properly populated and unique # value for each instantiation, so we hash the generic parameters here: