This commit is contained in:
Araq
2026-06-12 16:27:41 +02:00
parent 4307b1872a
commit 002d9ed0ef
8 changed files with 164 additions and 40 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 &

View File

@@ -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"

View File

@@ -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)

View File

@@ -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..<g.methods.len:
if g.methods[i].dispatcher.itemId == disp.itemId:
for existing in g.methods[i].methods:
if existing.itemId == m.itemId: return
g.methods[i].methods.add m
return
g.methods.add (methods: @[m], dispatcher: disp)
proc flushMethodReplays*(g: ModuleGraph) =
## Builds the dispatch buckets from the method registrations collected
## during module loading; called once every module of the program is
## loaded (`nifbackend.generateCode`).
for s in g.pendingMethodReplays:
registerLoadedMethod(g, s)
g.pendingMethodReplays.setLen 0
proc logGenericInstance*(g: ModuleGraph; inst: PSym) =
## Log a generic instance so it gets written to the NIF file.
## This is needed when generic instances are created during compile-time
@@ -861,6 +907,20 @@ when not defined(nimKochBootstrap):
g.loadedOps[x.op][x.key] = x.sym
of EnumToStrEntry:
g.loadedEnumToStringProcs[x.key] = x.sym
of MethodEntry:
# only `methodDef` registrations (empty key) rebuild dispatch
# buckets; the `addMethodToGeneric` flavor (typeKey key) announces
# the uninstantiated generic method, which must never enter a
# bucket (methodsPerGenericType replay is still a todo).
# Under `nim nifc` the replay is deferred: building a bucket forces
# the method's body, and a body loaded mid `loadModuleDependencies`
# registers modules it references in a different path context than
# the lazy loads during codegen do (`flushMethodReplays`).
if x.key.len == 0:
if g.config.cmd == cmdNifC:
g.pendingMethodReplays.add x.sym
else:
registerLoadedMethod(g, x.sym)
else:
discard
@@ -918,7 +978,7 @@ when not defined(nimKochBootstrap):
of ConverterEntry:
g.ifaces[fileIdx.int].converters.add x.sym
of MethodEntry:
discard "todo"
discard "dispatch buckets already rebuilt by registerLoadedHooks"
of GenericInstEntry:
raiseAssert "GenericInstEntry should not be in the NIF index"
of HookEntry, EnumToStrEntry:

View File

@@ -25,6 +25,7 @@ when defined(nimPreviewSlimSystem):
import ast, options, lineinfos, modulegraphs, cgendata, cgen,
pathutils, extccomp, msgs, modulepaths, idents, types, ast2nif, typekeys, dce,
cnif
from cgmeth import generateIfMethodDispatchers
import ic / replayer
proc loadModuleDependencies(g: ModuleGraph; mainFileIdx: FileIndex;
@@ -159,7 +160,13 @@ proc enforceDefRetention(g: ModuleGraph; mainPos: int;
# NB: reading `sym.kind` forces the lazy stub, so the kind is real
var action = 0 # un-reuse any visibly referencing TUs
if sym != nil:
if sym.kind in routineKinds or sym.kind == skConst:
if sfDispatcher in sym.flags:
# method dispatchers are synthesized from the whole
# program's method set and emitted into the main TU on
# every run; re-demanding the serialized sym would emit
# its (empty) serialized body
action = 1
elif sym.kind in routineKinds or sym.kind == skConst:
# routine and const definitions exist only by demand (the
# serialized top level holds just the eager init statements,
# not a proc listing!), and reused TUs never demand — so
@@ -407,15 +414,29 @@ proc finishModule(g: ModuleGraph; bmod: BModule) =
let initStmt = newNode(nkStmtList)
finalCodegenActions(g, bmod, initStmt)
# Dispatcher definitions live in the main TU only: their bodies enumerate
# the whole program's method set, so a copy inside a reusable TU would go
# stale when a method is added in an unrelated module (genProcLvl3 routes
# demand-driven dispatcher definitions to main the same way). Main is
# finished last, after all method registrations have been replayed.
if sfMainModule in bmod.module.flags:
for disp in getDispatchers(g):
if not containsOrIncl(bmod.declaredThings, disp.id):
genProcLvl3(bmod, disp)
# NB: the method dispatchers are emitted in `emitMethodDispatchers`,
# between the module loop and this finish loop: their bodies demand the
# method definitions, which can in turn demand definitions from modules
# the backend never loaded — and a TU demand-created during the LAST
# finishModule call would miss `modulesClosed` and never be written.
proc emitMethodDispatchers(g: ModuleGraph) =
## Synthesizes the method dispatcher bodies from the replayed dispatch
## buckets (`registerLoadedMethod`) and emits their definitions into the
## main TU. Main is regenerated on every run, so a dispatcher — whose
## body enumerates the whole program's method set — can never go stale
## inside a cached TU; cross-TU callers prototype it (see genProcLvl3).
let bl = BModuleList(g.backend)
var mainMod: BModule = nil
for m in bl.mods:
if m != nil and m.module != nil and sfMainModule in m.module.flags:
mainMod = m
break
if mainMod == nil: return
generateIfMethodDispatchers(g, mainMod.idgen)
for disp in getDispatchers(g):
if not containsOrIncl(mainMod.declaredThings, disp.id):
genProcLvl3(mainMod, disp)
proc generateCodeForModule(g: ModuleGraph; precomp: PrecompiledModule) =
## Generate C code for a single module.
@@ -484,6 +505,8 @@ proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) =
# This must happen BEFORE any code generation so that hooks are loaded into loadedOps
var nifFiles: seq[string] = @[toNifFilename(g.config, systemFileIdx)]
let modules = loadModuleDependencies(g, mainFileIdx, nifFiles)
# build the method dispatch buckets now that every module is loaded
flushMethodReplays(g)
phaseDone "load (" & $ (modules.len + 1) & " modules)"
if modules.len == 0:
rawMessage(g.config, errGenerated,
@@ -531,6 +554,8 @@ proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) =
for m in modules:
if not processed.containsOrIncl(m.module.position):
generateOrReuse(m)
emitMethodDispatchers(g)
phaseDone "cgen"
# during code generation of `main.nim` we can trigger the code generation

View File

@@ -197,6 +197,29 @@ proc semOpenSym(c: PContext, n: PNode, flags: TExprFlags, expectedType: PType,
# set symchoice node type back to None
n.typ = newTypeS(tyNone, c)
proc resolveOpenSymDotRhs(c: PContext, n: PNode): PNode =
## Resolves an `nkOpenSym` in the field position of a dot expression.
## The dot handling (`builtinFieldAccess`, `dotTransformation`) matches on
## the node kind of the RHS directly, so the wrapper cannot be left for
## `semExpr` to unwrap; without this the captured symbol degrades to a
## plain identifier that is then only looked up in the instantiation
## context. Mirrors `semOpenSym`: a symbol injected during instantiation
## under the current proc replaces the captured symbol, otherwise the
## captured node is used.
let inner = n[0]
result = inner
if inner.kind != nkSym: return
let id = newIdentNode(inner.sym.name, n.info)
c.isAmbiguous = false
let s2 = qualifiedLookUp(c, id, {})
if s2 != nil and not c.isAmbiguous and s2 != inner.sym:
# only consider symbols defined under the current proc:
var o = s2.owner
while o != nil:
if o == c.p.owner:
return id
o = o.owner
proc semSymChoice(c: PContext, n: PNode, flags: TExprFlags = {}, expectedType: PType = nil): PNode =
if n.kind == nkOpenSymChoice:
result = semOpenSym(c, n, flags, expectedType,
@@ -1524,6 +1547,9 @@ proc builtinFieldAccess(c: PContext; n: PNode; flags: var TExprFlags): PNode =
suggestExpr(c, n)
if exactEquals(c.config.m.trackPos, n[1].info): suggestExprNoCheck(c, n)
if n[1].kind == nkOpenSym:
n[1] = resolveOpenSymDotRhs(c, n[1])
var s = qualifiedLookUp(c, n, {checkAmbiguity, checkUndeclared, checkModule})
if s != nil:
if s.kind in OverloadableSyms: