diff --git a/compiler/ast2nif.nim b/compiler/ast2nif.nim index f81b7eea8f..be97c5d5b0 100644 --- a/compiler/ast2nif.nim +++ b/compiler/ast2nif.nim @@ -801,7 +801,10 @@ proc writeOp(w: var Writer; content: var TokenBuf; op: LogEntry) = content.add symToken(pool.syms.getOrIncl(w.toNifSymName(op.sym)), NoLineInfo) content.addParRi() of MethodEntry: - discard "to implement" + content.addParLe repMethodTag, NoLineInfo + content.add strToken(pool.strings.getOrIncl(op.key), NoLineInfo) + content.add symToken(pool.syms.getOrIncl(w.toNifSymName(op.sym)), NoLineInfo) + content.addParRi() of EnumToStrEntry: content.addParLe repEnumToStrTag, NoLineInfo content.add strToken(pool.strings.getOrIncl(op.key), NoLineInfo) diff --git a/compiler/cgen.nim b/compiler/cgen.nim index c180e8e5f1..1681e0a0cf 100644 --- a/compiler/cgen.nim +++ b/compiler/cgen.nim @@ -1368,26 +1368,18 @@ proc genProcBody(p: BProc; procBody: PNode) = p.blocks[0].sections[cpsInit].addAssignmentWithValue("nimErr_"): p.blocks[0].sections[cpsInit].addCall(cgsymValue(p.module, "nimErrorFlag")) -proc findMainBModule(g: BModuleList): BModule = - result = nil - for cand in g.mods: - if cand != nil and cand.module != nil and sfMainModule in cand.module.flags: - return cand - proc genProcLvl3*(m: BModule, prc: PSym) = if m.config.cmd == cmdNifC: fillBackendName(m, prc) if sfDispatcher in prc.flags and sfMainModule notin m.module.flags: - # A method dispatcher enumerates the whole program's method set; a - # definition inside a reusable TU goes stale as soon as a method is - # added in an unrelated module. Route every dispatcher definition - # into the main TU, which is regenerated on every run. + # A method dispatcher enumerates the whole program's method set: its + # body is synthesized by `generateIfMethodDispatchers` only after all + # modules have been generated, and its single definition is emitted + # into the main TU by `finishModule` (main is finished last and never + # reused, so the definition can never go stale inside a cached TU). + # Any demand before that point yields a prototype. genProcPrototype(m, prc) - let mainMod = findMainBModule(m.g) - if mainMod != nil: - if not containsOrIncl(mainMod.declaredThings, prc.id): - genProcLvl3(mainMod, prc) - return + return # inline procs are emitted into every using TU; they are never shared # across translation units, so cached/cross-TU dedup must not touch # them. Dispatchers always (re)define in main, never from the cache. @@ -1398,18 +1390,19 @@ proc genProcLvl3*(m: BModule, prc: PSym) = # already defined inside a reused TU from the previous run genProcPrototype(m, prc) return - if isSharedInstanceCName(m, prc) or - prc.itemId.module != m.module.position: - # one definition program-wide: shared instances by design; otherwise a - # definition redirected away from a reused TU — the first claimant's - # TU embeds it, everyone else declares it. The claim records the TU - # as well: with redirects the same symbol can be demanded into - # several TUs. - let claim = (sym: prc.itemId, tu: m.module.position) - if m.g.graph.icSharedDefOwner.hasKeyOrPut(key, claim) and - m.g.graph.icSharedDefOwner[key] != claim: - genProcPrototype(m, prc) - return + # one definition program-wide: the first claimant's TU embeds it, + # everyone else declares it. The claim records the TU as well: with + # redirects the same symbol can be demanded into several TUs. Home + # emissions must claim too — a hook's demand routing goes through the + # type-owner module (`findPendingModule` walks `s.owner`) while its + # eager emission uses the announcing module's TU; when the owner TU + # is reused, the very same symbol reaches this point through both + # paths and only the registry serializes them. + let claim = (sym: prc.itemId, tu: m.module.position) + if m.g.graph.icSharedDefOwner.hasKeyOrPut(key, claim) and + m.g.graph.icSharedDefOwner[key] != claim: + genProcPrototype(m, prc) + return if prc.itemId.module != m.module.position and not isBackendMinted(prc.itemId) and (prc.typ == nil or prc.typ.callConv != ccInline) and @@ -2886,7 +2879,12 @@ proc finalCodegenActions*(graph: ModuleGraph; m: BModule; n: PNode) = if m.g.forwardedProcs.len == 0: incl m.flags, objHasKidsValid - if optMultiMethods in m.g.config.globalOptions or + if m.config.cmd == cmdNifC: + # nifbackend synthesizes the dispatchers between the module loop + # and the finish loop (emitMethodDispatchers): TUs demand-created + # by the dispatcher bodies must still reach `modulesClosed` + discard + elif optMultiMethods in m.g.config.globalOptions or m.g.config.selectedGC notin {gcArc, gcOrc, gcAtomicArc, gcYrc} or vtables notin m.g.config.features: generateIfMethodDispatchers(graph, m.idgen) diff --git a/compiler/cgmeth.nim b/compiler/cgmeth.nim index 924d033144..a646b04746 100644 --- a/compiler/cgmeth.nim +++ b/compiler/cgmeth.nim @@ -180,6 +180,7 @@ proc methodDef*(g: ModuleGraph; idgen: IdGenerator; s: PSym) = g.methods[i].methods[0] != s: # already exists due to forwarding definition? localError(g.config, s.info, "method is not a base") + logMethodDef(g, s) return of No: discard of Invalid: @@ -191,6 +192,7 @@ proc methodDef*(g: ModuleGraph; idgen: IdGenerator; s: PSym) = else: g.bucketTable.inc(s.typ.firstParamType.skipTypes(skipPtrs).itemId) g.methods.add((methods: @[s], dispatcher: createDispatcher(s, g, idgen))) + logMethodDef(g, s) #echo "adding ", s.info if witness != nil: localError(g.config, s.info, "invalid declaration order; cannot attach '" & s.name.s & diff --git a/compiler/deps.nim b/compiler/deps.nim index 857b3504b4..005e683c35 100644 --- a/compiler/deps.nim +++ b/compiler/deps.nim @@ -629,6 +629,11 @@ proc generateBuildFile(c: DepContext): string = # them — phantom outputs that re-fire the build on every rerun). if c.config.selectedGC != gcUnselected: forwardedArgs.add "--mm:" & $c.config.selectedGC + # method dispatch semantics must match across the child processes: + # a child compiled without --multimethods:on builds different dispatch + # buckets (and rejects calls as ambiguous that multi-dispatch accepts) + if optMultiMethods in c.config.globalOptions: + forwardedArgs.add "--multimethods:on" # Define nifler command b.addTree "cmd" diff --git a/compiler/main.nim b/compiler/main.nim index 9dcc801657..4803248361 100644 --- a/compiler/main.nim +++ b/compiler/main.nim @@ -419,9 +419,14 @@ proc mainCommand*(graph: ModuleGraph) = # cmdM uses NIF files, not ROD files graph.config.symbolFiles = disabledSf setUseIc(true) + # vtable dispatch needs a whole-program vtable layout, which the + # per-module compilation model cannot provide (yet); methods dispatch + # through the classic if-chain dispatchers instead + excl conf.features, Feature.vtables commandCheck(graph) of cmdNifC: setUseIc(true) + excl conf.features, Feature.vtables # Generate C code from NIF files wantMainModule(conf) setOutFile(conf) diff --git a/compiler/modulegraphs.nim b/compiler/modulegraphs.nim index 56cdf7d09a..c908ce41d0 100644 --- a/compiler/modulegraphs.nim +++ b/compiler/modulegraphs.nim @@ -97,6 +97,9 @@ type # loads (reached only through system or demand-driven codegen) icFileReusedCnames*: HashSet[string] # their .c paths, so demand-created # BModules for them never write anything + pendingMethodReplays*: seq[PSym] # method registrations loaded under + # `nim nifc`, bucketed only after every + # module is loaded (`flushMethodReplays`) icPreserveDefs*: Table[int, seq[PSym]] # module position -> symbols whose # definitions the TU must keep emitting: # they were in its previous artifact and a @@ -443,6 +446,49 @@ proc addMethodToGeneric*(g: ModuleGraph; module: int; t: PType; col: int; m: PSy let ownerModule = if t.sym != nil: t.sym.itemId.module.int else: module g.opsLog.add LogEntry(kind: MethodEntry, module: ownerModule, key: key, sym: m) +proc logMethodDef*(g: ModuleGraph; s: PSym) = + ## Log a method registration (`cgmeth.methodDef`) so that importers and + ## the backend can rebuild the dispatch buckets (`g.methods`) from the + ## NIF replay log — the serialized method ast carries its dispatcher sym + ## at `dispatcherPos`, so replay reuses the original dispatcher that all + ## call sites reference by name (see `registerLoadedMethod`). + if g.config.cmd in {cmdNifC, cmdM}: + g.opsLog.add LogEntry(kind: MethodEntry, module: s.itemId.module.int, + key: "", sym: s) + +proc registerLoadedMethod*(g: ModuleGraph; m: PSym) = + ## Rebuild the dispatch buckets from a serialized method registration. + ## Buckets group the methods sharing a dispatcher; the dispatcher's BODY + ## does not exist in serialized form — `generateIfMethodDispatchers` + ## synthesizes it in the backend from the complete bucket. + template dbg(msg: string) = + when defined(icDbgMeth): + echo "[icMeth] replay ", (if m != nil: m.name.s else: "nil"), ": ", msg + if m == nil or sfDispatcher in m.flags: dbg "skip self/nil"; return + if m.ast == nil or dispatcherPos >= m.ast.len: + dbg "no dispatcherPos (len " & $(if m.ast != nil: m.ast.len else: -1) & ")" + return + let dn = m.ast[dispatcherPos] + if dn == nil or dn.kind != nkSym or dn.sym == nil: dbg "empty dispatcher slot"; return + let disp = dn.sym + if sfDispatcher notin disp.flags: dbg "slot sym not a dispatcher"; return + dbg "ok -> bucket of " & disp.name.s & "." & $disp.disamb + for i in 0..