mirror of
https://github.com/nim-lang/Nim.git
synced 2026-08-05 15:08:44 +00:00
IC: heavy refactoring of the pipelines
This commit is contained in:
@@ -198,6 +198,10 @@ type
|
||||
depSuffixes: HashSet[string] # module suffixes already emitted as `(import ...)` deps
|
||||
emittedBackendTypes: HashSet[int32] # backend-local type items already def'd this module
|
||||
emittedBackendSyms: HashSet[int32] # backend-local sym items already def'd this module
|
||||
lowering: bool # serializing the `lower` stage's `.t.nif` (per-entry self-contained)
|
||||
emittedFieldSyms: HashSet[ItemId] # lowering: derived env-field syms already def'd this entry
|
||||
inTypeReclist: int # >0 while writing a type's OWN reclist: fields must be SELF-CONTAINED
|
||||
# defs (the type can be seek-loaded in isolation), not entry-deduped uses
|
||||
|
||||
|
||||
proc isLocalSym(sym: PSym): bool {.inline.} =
|
||||
@@ -377,7 +381,13 @@ proc writeTypeDef(w: var Writer; dest: var TokenBuf; typ: PType) =
|
||||
writeType(w, dest, typ.typeInstImpl)
|
||||
#if typ.kind in {tyProc, tyIterator} and typ.nImpl != nil and typ.nImpl.kind != nkFormalParams:
|
||||
|
||||
# The reclist holds this type's OWN fields. A type can be force-loaded by
|
||||
# name in isolation (cg seeks the `.t.nif`/`.s.nif` index entry), so its
|
||||
# fields must be DEFS here, not entry-deduped SymUses whose def lives
|
||||
# elsewhere in the `(lowered)` entry and is never read by the seek.
|
||||
inc w.inTypeReclist
|
||||
writeNode(w, dest, typ.nImpl)
|
||||
dec w.inTypeReclist
|
||||
writeSym(w, dest, typ.ownerFieldImpl)
|
||||
writeSym(w, dest, typ.symImpl)
|
||||
|
||||
@@ -497,6 +507,18 @@ proc writeSymDef(w: var Writer; dest: var TokenBuf; sym: PSym) =
|
||||
writeLoc w, dest, sym.locImpl
|
||||
writeNode(w, dest, sym.constraintImpl)
|
||||
writeSym(w, dest, sym.instantiatedFromImpl)
|
||||
# The TRANSFORMED body (ic_ideas.md 2-way body): a routine run at compile time
|
||||
# (macro / VM transform / `static`) already has its lowered body — closure
|
||||
# `:env` and all — computed during sem; serialize it so the backend reuses it
|
||||
# instead of re-deriving (the divergence behind the t17.275 env class). An
|
||||
# empty `.` here means "same as the semchecked body OR to be found in the
|
||||
# `.t.nif`" (the `lower` stage fills that gap). Non-routines / not-yet-
|
||||
# transformed routines write the empty marker. (`transformedBodyImpl` only
|
||||
# exists in the routine branch of the `TSym` variant.)
|
||||
if sym.kindImpl in routineKinds:
|
||||
writeNode(w, dest, sym.transformedBodyImpl)
|
||||
else:
|
||||
dest.addDotToken
|
||||
dest.addParRi
|
||||
|
||||
|
||||
@@ -517,9 +539,24 @@ proc shouldWriteSymDef(w: var Writer; sym: PSym): bool {.inline.} =
|
||||
return true # Normal case for global symbols
|
||||
return false
|
||||
|
||||
proc isLoweredPerEntryField(w: Writer; sym: PSym): bool {.inline.} =
|
||||
## In the `lower` stage every entry (each `(lowered)` body AND each `@bk` type
|
||||
## def, which carries its fields inline) is loaded independently, so it must be
|
||||
## SELF-CONTAINED. A derived closure-env FIELD is module-homed (its id derives
|
||||
## from the captured local, NOT `@bk` — see itemids.derivedFieldId) so
|
||||
## `shouldWriteSymDef` would seal it after the first entry and later entries
|
||||
## would reference a def that their indexed copy does not contain. Re-emit it
|
||||
## as a full def per entry, deduped within the entry via `emittedFieldSyms`.
|
||||
w.lowering and sym.kindImpl == skField and not sym.itemId.isBackendMinted
|
||||
|
||||
proc writeSym(w: var Writer; dest: var TokenBuf; sym: PSym) =
|
||||
if sym == nil:
|
||||
dest.addDotToken()
|
||||
elif isLoweredPerEntryField(w, sym):
|
||||
if not w.emittedFieldSyms.containsOrIncl(sym.itemId):
|
||||
writeSymDef(w, dest, sym)
|
||||
else:
|
||||
dest.addSymUse pool.syms.getOrIncl(w.toNifSymName(sym)), NoLineInfo
|
||||
elif sym.itemId.isBackendMinted:
|
||||
# Process-local backend sym (closure env field / hidden `:env` param): emit a
|
||||
# MODULE-LOCAL `@bk` def the first time, reference it after. Per-Writer dedup.
|
||||
@@ -556,11 +593,18 @@ proc writeSymNode(w: var Writer; dest: var TokenBuf; n: PNode; sym: PSym) =
|
||||
# dedup shared with `writeSym`), regardless of module: their itemId.module is
|
||||
# the systemModule of `vmTransfIdgen`, so `shouldWriteSymDef` (which gates on
|
||||
# currentModule) would otherwise only ever emit a SymUse → dangling def.
|
||||
let perEntryField = isLoweredPerEntryField(w, sym)
|
||||
# A field reached while writing its own type's reclist MUST be a self-contained
|
||||
# def: the type can be seek-loaded by name in isolation, so a deduped SymUse
|
||||
# (whose def lives elsewhere in the entry) would resolve to nil.
|
||||
let reclistField = w.lowering and w.inTypeReclist > 0 and sym.kindImpl == skField
|
||||
let wantDef =
|
||||
if sym.itemId.isBackendMinted: not w.emittedBackendSyms.containsOrIncl(sym.itemId.item)
|
||||
if reclistField: true
|
||||
elif sym.itemId.isBackendMinted: not w.emittedBackendSyms.containsOrIncl(sym.itemId.item)
|
||||
elif perEntryField: not w.emittedFieldSyms.containsOrIncl(sym.itemId)
|
||||
else: shouldWriteSymDef(w, sym)
|
||||
if wantDef:
|
||||
if not sym.itemId.isBackendMinted: sym.state = Sealed
|
||||
if not sym.itemId.isBackendMinted and not perEntryField and not reclistField: sym.state = Sealed
|
||||
if nodeTyp != n.sym.typImpl:
|
||||
dest.buildTree hiddenTypeTag, trLineInfo(w, n.info):
|
||||
writeType(w, dest, nodeTyp)
|
||||
@@ -674,6 +718,7 @@ var implTag = registerTag("implementation")
|
||||
var reexpModTag = registerTag("reexpmod")
|
||||
var offerTag = registerTag("offer")
|
||||
var typeOfferTag = registerTag("toffer")
|
||||
var loweredTag = registerTag("lowered")
|
||||
|
||||
proc registerNifAstTags*() =
|
||||
## (Re)registers ast2nif's NIF tags explicitly. The top-level `registerTag`
|
||||
@@ -706,6 +751,7 @@ proc registerNifAstTags*() =
|
||||
reexpModTag = registerTag("reexpmod")
|
||||
offerTag = registerTag("offer")
|
||||
typeOfferTag = registerTag("toffer")
|
||||
loweredTag = registerTag("lowered")
|
||||
|
||||
proc writeNode(w: var Writer; dest: var TokenBuf; n: PNode; forAst = false) =
|
||||
if n == nil:
|
||||
@@ -1441,7 +1487,7 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode;
|
||||
content.addParRi()
|
||||
|
||||
let m = modname(w.currentModule, w.infos.config)
|
||||
let nifFilename = AbsoluteFile(m).changeFileExt(".nif")
|
||||
let nifFilename = AbsoluteFile(m).changeFileExt(".s.nif")
|
||||
let d = completeGeneratedFilePath(config, nifFilename).string
|
||||
|
||||
var dest = createTokenBuf(600)
|
||||
@@ -1472,6 +1518,71 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode;
|
||||
writeImplCookie(config, thisModule, dest, ifaceHex)
|
||||
writeEdgesFile(config, thisModule, implDeps)
|
||||
|
||||
proc collectLoweredLocals(w: var Writer; n: PNode) =
|
||||
## Record every transform-created local (a `Complete`, non-`@bk` sym owned by
|
||||
## the module being serialized) in `w.locals` so it is written SUFFIX-LESS as
|
||||
## an inline def. Pre-existing entities (params, result, callees) were sealed
|
||||
## before serialization, so they are skipped here and emit as index SymUses.
|
||||
if n == nil: return
|
||||
if n.kind == nkSym and n.sym != nil:
|
||||
let s = n.sym
|
||||
if not s.itemId.isBackendMinted and s.itemId.module == w.currentModule and
|
||||
s.state == Complete:
|
||||
w.locals.incl s.itemId
|
||||
for i in 0 ..< n.safeLen:
|
||||
collectLoweredLocals(w, n[i])
|
||||
|
||||
proc serializeLoweredBodies*(config: ConfigRef; ownerModule: int32;
|
||||
entries: openArray[tuple[name: string; body: PNode]];
|
||||
hooks: openArray[LogEntry];
|
||||
outfile: string) =
|
||||
## Write the `lower` backend stage's `.t.nif`. Three sections, all covered by
|
||||
## the file's embedded index (which the `cg` loader registers as the module's
|
||||
## SECOND index/stream — see loadLoweredBodies):
|
||||
## - `(repdestroy/repcopy/... key sym)`: the type-bound ops the lower stage
|
||||
## lifted while transforming (closure-env `=destroy` etc.). `cg`'s
|
||||
## `registerLoadedHooks` re-attaches them so `injectDestructorCalls` (kept in
|
||||
## cg) resolves them via `getAttachedOp`'s key fallback.
|
||||
## - the hook ROUTINES as full `(sd)`: NEW in the lower stage (no `.s.nif`
|
||||
## signature), so serialize sig + transformed body whole; cg loads them via
|
||||
## the `.t.nif` index when the HookEntry's SymUse resolves, then demand-emits.
|
||||
## - owned routines' transformed bodies as `(lowered "<nifname>" <body>)`: only
|
||||
## the body (signature comes from the `.s.nif`); applyLoweredBodies sets it on
|
||||
## the existing sym.
|
||||
## A `@bk` closure-env type/sym referenced anywhere is emitted inline AND
|
||||
## indexed, so it resolves through the embedded index regardless of section.
|
||||
var w = Writer(infos: LineInfoWriter(config: config), currentModule: ownerModule)
|
||||
w.inProc = 1 # we are serializing routine *bodies*
|
||||
w.lowering = true
|
||||
var dest = createTokenBuf(256)
|
||||
createStmtList(dest, NoLineInfo)
|
||||
for op in hooks:
|
||||
writeOp(w, dest, op)
|
||||
var emittedHooks = initHashSet[int32]()
|
||||
for op in hooks:
|
||||
if op.sym != nil and op.sym.kindImpl in routineKinds and
|
||||
not emittedHooks.containsOrIncl(op.sym.itemId.item):
|
||||
w.emittedBackendTypes.clear()
|
||||
w.emittedBackendSyms.clear()
|
||||
w.emittedFieldSyms.clear()
|
||||
w.locals.clear()
|
||||
writeSymDef(w, dest, op.sym)
|
||||
for e in entries:
|
||||
# Each `(lowered ...)` entry is loaded independently, so it must be
|
||||
# SELF-CONTAINED: reset the per-Writer backend-local (@bk) dedup so every
|
||||
# entry re-emits the def of any closure-env type/sym it references.
|
||||
w.emittedBackendTypes.clear()
|
||||
w.emittedBackendSyms.clear()
|
||||
w.emittedFieldSyms.clear()
|
||||
w.locals.clear()
|
||||
collectLoweredLocals(w, e.body)
|
||||
dest.addParLe loweredTag, NoLineInfo
|
||||
dest.addStrLit e.name
|
||||
writeNode(w, dest, e.body)
|
||||
dest.addParRi
|
||||
dest.addParRi()
|
||||
writeFile(dest, outfile)
|
||||
|
||||
# --------------------------- Loader (lazy!) -----------------------------------------------
|
||||
|
||||
proc nodeKind(n: Cursor): TNodeKind {.inline.} =
|
||||
@@ -1533,6 +1644,15 @@ type
|
||||
suffix: string
|
||||
contentStart: int # stream offset of the module body, so a full-AST load can
|
||||
# rewind after lazy symbol loads moved the cursor
|
||||
# The module's `.t.nif` (the `lower` stage's transformed bodies): a SECOND
|
||||
# embedded-index + stream consulted when the main `.s.nif` index misses. The
|
||||
# transformed bodies' backend-minted (`@bk`) entities (closure-env types/syms,
|
||||
# temporaries) live ONLY here; they are named with this module's suffix, so
|
||||
# `createTypeStub`/`loadSymStub` look them up here on a `.s.nif` miss and load
|
||||
# them from `tStream` (see applyLoweredBodies). "Just use NIF's embedded index."
|
||||
hasTIndex: bool
|
||||
tStream: nifstreams.Stream
|
||||
tIndex: Table[string, NifIndexEntry]
|
||||
|
||||
DecodeContext* = object
|
||||
infos: LineInfoWriter
|
||||
@@ -1568,8 +1688,8 @@ proc loadedState(c: DecodeContext): ItemState {.inline.} =
|
||||
if c.infos.config.cmd == cmdNifC: Complete else: Sealed
|
||||
|
||||
proc cursorFromIndexEntry(c: var DecodeContext; module: FileIndex; entry: NifIndexEntry;
|
||||
buf: var TokenBuf): Cursor =
|
||||
let s = addr c.mods[module].stream
|
||||
buf: var TokenBuf; fromT = false): Cursor =
|
||||
let s = if fromT: addr c.mods[module].tStream else: addr c.mods[module].stream
|
||||
s.r.jumpTo entry.offset
|
||||
# A seek-load is self-contained: its tokens must decode their relative line
|
||||
# info against `entry.info` ALONE. The stream's `parents` stack can be left at
|
||||
@@ -1631,7 +1751,7 @@ proc moduleId(c: var DecodeContext; suffix: string; flags: set[LoadFlag] = {}):
|
||||
# but haven't had their NIF index loaded yet
|
||||
let hasEntry = c.mods.hasKey(result)
|
||||
if not hasEntry or AlwaysLoadInterface in flags:
|
||||
let modFile = (getNimcacheDir(c.infos.config) / RelativeFile(suffix & ".nif")).string
|
||||
let modFile = (getNimcacheDir(c.infos.config) / RelativeFile(suffix & ".s.nif")).string
|
||||
if not fileExists(modFile):
|
||||
raiseAssert "NIF file not found for module suffix '" & suffix & "': " & modFile &
|
||||
". This can happen when loading a module from NIF that references another module " &
|
||||
@@ -1723,10 +1843,17 @@ proc createTypeStub(c: var DecodeContext; t: SymId): PType =
|
||||
let realSuffix = if isBk: suffix[0 ..< suffix.len - BackendLocalMarker.len] else: suffix
|
||||
let modIdx = moduleId(c, realSuffix).int32
|
||||
let id = if isBk: backendItemId(modIdx, itemVal) else: itemId(modIdx, itemVal)
|
||||
let ii = addr c.mods[id.module.FileIndex].index
|
||||
let offs = ii[].getOrDefault(name)
|
||||
let modFi = id.module.FileIndex
|
||||
let ii = addr c.mods[modFi].index
|
||||
var offs = ii[].getOrDefault(name)
|
||||
if offs.offset == 0:
|
||||
raiseAssert "symbol has no offset: " & name
|
||||
# A backend-minted (`@bk`) type produced by the `lower` stage lives in this
|
||||
# module's `.t.nif`, not its `.s.nif` index. Resolve it through the `.t.nif`
|
||||
# embedded index (loadType picks `tStream` for a tIndex-only name).
|
||||
if c.mods[modFi].hasTIndex:
|
||||
offs = c.mods[modFi].tIndex.getOrDefault(name)
|
||||
if offs.offset == 0:
|
||||
raiseAssert "symbol has no offset: " & name
|
||||
result = PType(itemId: id, uniqueId: id, kind: TTypeKind(k), state: Partial)
|
||||
c.types[name] = (result, offs)
|
||||
|
||||
@@ -1821,7 +1948,16 @@ proc loadSymStub(c: var DecodeContext; t: SymId; thisModule: string;
|
||||
inc val[]
|
||||
let id = if isBk: backendItemId(module.int32, val[]) else: itemId(module.int32, val[])
|
||||
|
||||
let offs = c.mods[module].index.getOrDefault(symAsStr)
|
||||
var offs = c.mods[module].index.getOrDefault(symAsStr)
|
||||
if offs.offset == 0 and c.mods[module].hasTIndex:
|
||||
# A sym produced by the `lower` stage lives in this module's `.t.nif`, not
|
||||
# its `.s.nif` index. This covers BOTH backend-minted (`@bk`) entities
|
||||
# (closure `:env` param, `:tmp`/`res` temporaries) AND module-homed derived
|
||||
# closure-env FIELDS (e.g. a captured `i`): the latter have a normal
|
||||
# module suffix but are added to the env object only in the backend, so
|
||||
# they are indexed solely in `.t.nif`. Resolve through that embedded index
|
||||
# (loadSym picks `tStream` for a tIndex-only name).
|
||||
offs = c.mods[module].tIndex.getOrDefault(symAsStr)
|
||||
if offs.offset == 0:
|
||||
# Only module/package self-syms are never written as `(sd)` entries, so a
|
||||
# missing index offset means this is such a sym — typically the OWNER of an
|
||||
@@ -1902,7 +2038,12 @@ proc loadTypeFromCursor(c: var DecodeContext; n: var Cursor; t: PType; localSyms
|
||||
raiseAssert "(td) expected"
|
||||
|
||||
var scanCursor = n # copy cursor at start of type
|
||||
let typesModule = parseSymName(pool.syms[n.firstSon.symId]).module
|
||||
var typesModule = parseSymName(pool.syms[n.firstSon.symId]).module
|
||||
if typesModule.endsWith(BackendLocalMarker):
|
||||
# A backend-minted (`@bk`) type's name carries the marker in its module part;
|
||||
# strip it so the nested-local pre-scan resolves the real module, not a
|
||||
# nonexistent `<suffix>@bk.nif`.
|
||||
typesModule = typesModule[0 ..< typesModule.len - BackendLocalMarker.len]
|
||||
extractLocalSymsFromTree(c, scanCursor, typesModule, localSyms)
|
||||
|
||||
inc n # move past (td
|
||||
@@ -1941,8 +2082,22 @@ proc loadType*(c: var DecodeContext; t: PType) =
|
||||
if t.state != Partial: return
|
||||
t.state = c.loadedState
|
||||
var buf = createTokenBuf(30)
|
||||
let typeName = typeToNifSym(t, c.infos.config)
|
||||
var n = cursorFromIndexEntry(c, t.itemId.module.FileIndex, c.types[typeName][1], buf)
|
||||
# A backend-minted (`@bk`) closure-env type produced by the `lower` stage lives
|
||||
# ONLY in the `.t.nif` and is keyed by its `@bk` name (see nifTypeName), not the
|
||||
# canonical `typeToNifSym` (which asserts non-`@bk`). Reconstruct that name so a
|
||||
# Partial `@bk` stub that escaped the inline pre-scan can still be force-loaded.
|
||||
let typeName =
|
||||
if t.uniqueId.isBackendMinted:
|
||||
"`t" & $ord(t.kind) & "." & $t.uniqueId.item & "." &
|
||||
modname(t.itemId.module, c.infos.config) & BackendLocalMarker
|
||||
else:
|
||||
typeToNifSym(t, c.infos.config)
|
||||
let modFi = t.itemId.module.FileIndex
|
||||
# A name resolved through the `.t.nif` (tIndex) — a `lower`-stage closure-env
|
||||
# type — must seek in `tStream`, not the `.s.nif` stream.
|
||||
let fromT = c.mods[modFi].hasTIndex and not c.mods[modFi].index.hasKey(typeName) and
|
||||
c.mods[modFi].tIndex.hasKey(typeName)
|
||||
var n = cursorFromIndexEntry(c, modFi, c.types[typeName][1], buf, fromT = fromT)
|
||||
var localSyms = initTable[string, PSym]()
|
||||
loadTypeFromCursor(c, n, t, localSyms)
|
||||
|
||||
@@ -2027,6 +2182,15 @@ proc loadSymFromCursor(c: var DecodeContext; s: PSym; n: var Cursor; thisModule:
|
||||
loadLoc c, n, s.locImpl
|
||||
s.constraintImpl = loadNode(c, n, thisModule, localSyms)
|
||||
s.instantiatedFromImpl = loadSymStub(c, n, thisModule, localSyms)
|
||||
# The TRANSFORMED body slot (see writeSymDef). Reconstruct it ONLY in the
|
||||
# backend (`cmdNifC`), where `transformBody` short-circuits on it; during
|
||||
# frontend sem (`cmdM`) skip the tokens — a dependent never needs a foreign
|
||||
# routine's lowered body, and reconstructing one must not perturb effect/
|
||||
# exception inference (the "never change frontend node-typing for IC" rule).
|
||||
if c.infos.config.cmd == cmdNifC and s.kindImpl in routineKinds:
|
||||
s.transformedBodyImpl = loadNode(c, n, thisModule, localSyms)
|
||||
else:
|
||||
skip n
|
||||
skipParRi n
|
||||
|
||||
proc loadSym*(c: var DecodeContext; s: PSym) =
|
||||
@@ -2035,7 +2199,10 @@ proc loadSym*(c: var DecodeContext; s: PSym) =
|
||||
var buf = createTokenBuf(30)
|
||||
let symsModule = s.itemId.module.FileIndex
|
||||
let nifname = globalName(s, c.infos.config)
|
||||
var n = cursorFromIndexEntry(c, symsModule, c.syms[nifname][1], buf)
|
||||
# A `@bk` sym resolved through the `.t.nif` (tIndex) seeks in `tStream`.
|
||||
let fromT = c.mods[symsModule].hasTIndex and not c.mods[symsModule].index.hasKey(nifname) and
|
||||
c.mods[symsModule].tIndex.hasKey(nifname)
|
||||
var n = cursorFromIndexEntry(c, symsModule, c.syms[nifname][1], buf, fromT = fromT)
|
||||
|
||||
expect n, ParLe
|
||||
if n.tagId != sdefTag:
|
||||
@@ -2053,6 +2220,149 @@ proc loadSym*(c: var DecodeContext; s: PSym) =
|
||||
inc n
|
||||
loadSymFromCursor(c, s, n, c.mods[symsModule].suffix, localSyms)
|
||||
|
||||
proc sealLoadedBackendEntities*(c: var DecodeContext) =
|
||||
## Before the `lower` stage serializes its transformed bodies, mark every
|
||||
## index-loaded sym/type `Sealed`. The backend loads them `Complete` (mutable
|
||||
## for the transform); without this, `writeNode`'s `shouldWriteSymDef` would
|
||||
## emit a duplicate `(sd)`/`(td)` def for a param/result/existing-local/owner
|
||||
## type — and the `cg` body-loader would then bind the body's `result` to a
|
||||
## FRESH sym instead of the one in `prc.ast[resultPos]` (the #6/#7 "result
|
||||
## cannot be captured" class). Sealed ⟹ SymUse ⟹ resolved via `cg`'s module
|
||||
## index. The transform-CREATED entities are absent from `c.syms`/`c.types`
|
||||
## (or are backend-minted), so they stay `Complete`/`@bk` and still get the
|
||||
## inline defs the loader's local-sym pre-scan needs.
|
||||
for _, v in c.syms:
|
||||
if v[0] != nil and v[0].state == Complete: v[0].state = Sealed
|
||||
for _, v in c.types:
|
||||
if v[0] != nil and v[0].state == Complete: v[0].state = Sealed
|
||||
|
||||
proc preloadLoweredDefs(c: var DecodeContext; n: var Cursor; thisModule: string;
|
||||
localSyms: var Table[string, PSym]) =
|
||||
## One pre-scan over a `.t.nif` body that fully loads EVERY inline def it
|
||||
## carries before the body is decoded: suffix-less locals into `localSyms`,
|
||||
## backend-minted `@bk` syms into `c.syms`, `@bk` types into `c.types`. These
|
||||
## transform-created entities have NO module index entry, so a later reference
|
||||
## — in the body, or inside a loaded type that `typeKey`/codegen later walks
|
||||
## (where the index-based force-load would assert) — must find them already
|
||||
## loaded. The writer emits defs before uses, so a forward walk that loads each
|
||||
## in place suffices.
|
||||
if n.kind != ParLe:
|
||||
inc n
|
||||
return
|
||||
var depth = 0
|
||||
while true:
|
||||
if n.kind == ParLe:
|
||||
if n.tagId == sdefTag:
|
||||
let nm = n.firstSon
|
||||
if nm.kind == SymbolDef:
|
||||
let symName = pool.syms[nm.symId]
|
||||
let sn = parseSymName(symName)
|
||||
if sn.module.len == 0:
|
||||
if symName notin localSyms:
|
||||
let module = moduleId(c, thisModule)
|
||||
let val = addr c.mods[module].symCounter
|
||||
inc val[]
|
||||
let sym = PSym(itemId: itemId(module.int32, val[]), kindImpl: skStub,
|
||||
name: c.cache.getIdent(sn.name), disamb: sn.count.int32,
|
||||
state: Complete)
|
||||
localSyms[symName] = sym
|
||||
inc n
|
||||
loadSymFromCursor(c, sym, n, thisModule, localSyms)
|
||||
sym.state = c.loadedState
|
||||
continue
|
||||
elif sn.module.endsWith(BackendLocalMarker):
|
||||
let sym = c.loadSymStub(nm.symId, thisModule, localSyms)
|
||||
if sym.state == Partial:
|
||||
sym.state = c.loadedState
|
||||
inc n
|
||||
loadSymFromCursor(c, sym, n, thisModule, localSyms)
|
||||
continue
|
||||
elif n.tagId == tdefTag:
|
||||
let nm = n.firstSon
|
||||
if nm.kind == SymbolDef and pool.syms[nm.symId].endsWith(BackendLocalMarker):
|
||||
discard loadTypeStub(c, n, localSyms)
|
||||
continue
|
||||
inc depth
|
||||
elif n.kind == ParRi:
|
||||
dec depth
|
||||
if depth == 0:
|
||||
inc n
|
||||
break
|
||||
inc n
|
||||
|
||||
proc resolveHookSym*(c: var DecodeContext; symId: nifstreams.SymId): PSym
|
||||
|
||||
proc repTagToOp(tagId: TagId): (bool, TTypeAttachedOp) =
|
||||
## Map a `(rep…)` hook tag to its attached-op kind (and whether it IS one).
|
||||
if tagId == repDestroyTag: (true, attachedDestructor)
|
||||
elif tagId == repCopyTag: (true, attachedAsgn)
|
||||
elif tagId == repWasMovedTag: (true, attachedWasMoved)
|
||||
elif tagId == repDupTag: (true, attachedDup)
|
||||
elif tagId == repSinkTag: (true, attachedSink)
|
||||
elif tagId == repTraceTag: (true, attachedTrace)
|
||||
elif tagId == repDeepCopyTag: (true, attachedDeepCopy)
|
||||
else: (false, attachedDestructor)
|
||||
|
||||
proc loadLoweredBodies*(c: var DecodeContext; module: FileIndex; suffix: string;
|
||||
infile: string; loadBodies = true):
|
||||
tuple[bodies: seq[tuple[name: string; body: PNode]]; hooks: seq[LogEntry]] =
|
||||
## Reconstruct the `lower` stage's `.t.nif`. Registers its embedded index as the
|
||||
## module's SECOND index/stream (`tIndex`/`tStream`) so every backend-minted
|
||||
## (`@bk`) closure-env entity resolves through NIF's own index — `createTypeStub`/
|
||||
## `loadSymStub` fall through to it on a `.s.nif` miss, `loadType`/`loadSym` seek
|
||||
## `tStream`. Returns the owned routines' transformed bodies (the `(lowered …)`
|
||||
## entries) and the lifted type-bound ops (`(rep… key sym)`); the hook ROUTINES'
|
||||
## `(sd)` defs are loaded lazily through the index when their op's sym resolves.
|
||||
result = (@[], @[])
|
||||
if not fileExists(infile): return
|
||||
var tstream = nifstreams.open(infile)
|
||||
let tindex = readEmbeddedIndex(tstream) # leaves the cursor at the content start
|
||||
let contentStart = offset(tstream.r)
|
||||
c.mods[module].tStream = tstream
|
||||
c.mods[module].tIndex = tindex
|
||||
c.mods[module].hasTIndex = true
|
||||
# Parse the WHOLE content up front: the per-body loads below lazily seek
|
||||
# `tStream` for `@bk` entities, which would otherwise clobber a live walk cursor.
|
||||
var buf = createTokenBuf(256)
|
||||
tstream.r.jumpTo contentStart
|
||||
nifcursors.parse(tstream, buf, NoLineInfo)
|
||||
var n = beginRead(buf)
|
||||
if n.kind != ParLe: return
|
||||
inc n # into (stmts -> flags dot
|
||||
inc n # -> type dot
|
||||
inc n # -> first entry or ParRi
|
||||
while n.kind == ParLe:
|
||||
if n.tagId == loweredTag:
|
||||
if not loadBodies:
|
||||
# A dependency: register its `.t.nif` (tIndex + hooks) so the consumer
|
||||
# can destroy the dep's closures, but don't reconstruct its bodies (only
|
||||
# the dep's OWN cg emits them).
|
||||
skip n
|
||||
continue
|
||||
inc n # -> StringLit name
|
||||
let name = pool.strings[n.litId]
|
||||
inc n # -> body tree
|
||||
var localSyms = initTable[string, PSym]()
|
||||
var scanCursor = n
|
||||
extractLocalSymsFromTree(c, scanCursor, suffix, localSyms)
|
||||
let body = loadNode(c, n, suffix, localSyms)
|
||||
result.bodies.add (name, body)
|
||||
skipParRi n # close (lowered ...)
|
||||
else:
|
||||
let (isHook, op) = repTagToOp(n.tagId)
|
||||
if isHook:
|
||||
inc n # -> StringLit key
|
||||
let key = pool.strings[n.litId]
|
||||
inc n # -> Symbol sym
|
||||
let sym = resolveHookSym(c, n.symId)
|
||||
inc n
|
||||
if sym != nil:
|
||||
result.hooks.add LogEntry(kind: HookEntry, op: op, module: module.int,
|
||||
key: key, sym: sym)
|
||||
skipParRi n
|
||||
else:
|
||||
skip n # a hook routine's `(sd)` def — loaded lazily via the index
|
||||
|
||||
|
||||
template withNode(c: var DecodeContext; n: var Cursor; result: PNode; kind: TNodeKind; body: untyped) =
|
||||
let info = c.infos.oldLineInfo(n.info)
|
||||
@@ -2126,9 +2436,47 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string;
|
||||
loadSymFromCursor(c, sym, n, thisModule, localSyms)
|
||||
sym.state = c.loadedState # mark as fully loaded
|
||||
result = newSymNode(sym, info)
|
||||
else:
|
||||
elif sn.module.endsWith(BackendLocalMarker):
|
||||
# A backend-minted (`@bk`) def lives ONLY inline in this `.t.nif` body
|
||||
# (not in any module index): create/find its cached stub and FILL it
|
||||
# from the sdef instead of skipping (which would leave the skModule/
|
||||
# Partial stub `loadSymStub` made unresolved).
|
||||
sym = c.loadSymStub(name.symId, thisModule, localSyms)
|
||||
skip n # skip the entire sdef for indexed symbols
|
||||
if sym.state == Partial:
|
||||
sym.state = c.loadedState
|
||||
inc n # skip `sd` tag
|
||||
loadSymFromCursor(c, sym, n, thisModule, localSyms)
|
||||
else:
|
||||
skip n
|
||||
result = newSymNode(sym, info)
|
||||
result.flags.incl nfLazyType
|
||||
else:
|
||||
# A module-homed inline sdef. Normally its def lives in that module's
|
||||
# index and is loaded lazily, so we skip the inline copy. BUT a
|
||||
# transform-created closure-env FIELD (`x0.0.clo`) is module-homed yet
|
||||
# lives ONLY inline in this `.t.nif` reclist — it has no index entry.
|
||||
# Skipping it leaves a nil-typed `skModule` fallback stub (from
|
||||
# loadSymStub's "no offset" path) and codegen of the env struct then
|
||||
# dereferences a nil field type. Detect the unindexed case and FILL
|
||||
# the sym from the inline def instead.
|
||||
let m = moduleId(c, sn.module)
|
||||
let indexed = c.mods[m].index.hasKey(symName) or
|
||||
(c.mods[m].hasTIndex and c.mods[m].tIndex.hasKey(symName))
|
||||
if indexed:
|
||||
sym = c.loadSymStub(name.symId, thisModule, localSyms)
|
||||
skip n # skip the entire sdef for indexed symbols
|
||||
else:
|
||||
sym = c.syms.getOrDefault(symName)[0]
|
||||
if sym == nil:
|
||||
let val = addr c.mods[m].symCounter
|
||||
inc val[]
|
||||
sym = PSym(itemId: itemId(m.int32, val[]), kindImpl: skStub,
|
||||
name: c.cache.getIdent(sn.name), disamb: sn.count.int32,
|
||||
state: Partial)
|
||||
c.syms[symName] = (sym, NifIndexEntry())
|
||||
sym.state = c.loadedState
|
||||
inc n # skip `sd` tag
|
||||
loadSymFromCursor(c, sym, n, thisModule, localSyms)
|
||||
result = newSymNode(sym, info)
|
||||
result.flags.incl nfLazyType
|
||||
of typeDefTagName:
|
||||
@@ -2289,7 +2637,7 @@ proc moduleSymbolStubs*(c: var DecodeContext; module: FileIndex): seq[PSym] =
|
||||
|
||||
proc toNifFilename*(conf: ConfigRef; f: FileIndex): string =
|
||||
let suffix = moduleSuffix(conf, f)
|
||||
result = toGeneratedFile(conf, AbsoluteFile(suffix), ".nif").string
|
||||
result = toGeneratedFile(conf, AbsoluteFile(suffix), ".s.nif").string
|
||||
|
||||
proc resolveSym(c: var DecodeContext; symAsStr: string; alsoConsiderPrivate: bool): PSym =
|
||||
result = c.syms.getOrDefault(symAsStr)[0]
|
||||
@@ -2311,6 +2659,11 @@ proc resolveSym(c: var DecodeContext; symAsStr: string; alsoConsiderPrivate: boo
|
||||
# Try the format without module suffix
|
||||
let localKey = sn.name & "." & $sn.count & "."
|
||||
offs = c.mods[module].index.getOrDefault(localKey)
|
||||
if offs.offset == 0 and c.mods[module].hasTIndex:
|
||||
# A `lower`-stage entity (an `@bk` hook routine, OR a module-homed derived
|
||||
# closure-env field added only in the backend) lives in the module's
|
||||
# `.t.nif`, not its `.s.nif`: resolve it through the second (tIndex) index.
|
||||
offs = c.mods[module].tIndex.getOrDefault(symAsStr)
|
||||
if offs.offset == 0:
|
||||
return nil
|
||||
if not alsoConsiderPrivate and offs.vis == Hidden:
|
||||
|
||||
@@ -358,6 +358,52 @@ proc readCnifHeads*(f: string): CnifHeads =
|
||||
endRead(c)
|
||||
result.valid = sawMeta and version == CnifVersion
|
||||
|
||||
proc writeLoweredArtifact*(outfile: string; entries: openArray[string]) =
|
||||
## The `.t.nif` "lowered" artifact: one `(lowered "<nifname>" <body>)` per
|
||||
## routine the module OWNS, written by the per-module `lower` backend stage
|
||||
## for the `cg` stage to read instead of re-deriving the transformed body
|
||||
## (re-derivation in each parallel `cg` process is what makes a closure
|
||||
## `:env`'s identity diverge across modules — see transf.transformBody).
|
||||
##
|
||||
## SKELETON: every body is the empty-marker `.` ("transformed body == sem
|
||||
## body"), so `cg` falls back to its own `transformBody` and output stays
|
||||
## byte-identical. The real transformed body fills this slot in a later step;
|
||||
## the `.` then means "unchanged by lowering" (the dedup Araq sketched).
|
||||
var b = nifbuilder.open(outfile)
|
||||
b.withTree "stmts":
|
||||
for name in entries:
|
||||
b.withTree "lowered":
|
||||
b.addStrLit name
|
||||
b.addEmpty()
|
||||
b.close()
|
||||
|
||||
proc readLoweredArtifact*(f: string): seq[string] =
|
||||
## The routine NIF names recorded in a `.t.nif`. (Bodies are not returned
|
||||
## yet: the skeleton records only empty-markers; reading proves the artifact
|
||||
## round-trips and that the `cg` rule depends on it.)
|
||||
result = @[]
|
||||
if not fileExists(f): return
|
||||
var pool = newPool()
|
||||
var tags = newTagPool()
|
||||
let stmtsTag = tags.registerTag("stmts")
|
||||
let loweredTag = tags.registerTag("lowered")
|
||||
var buf = parseFromFile(f, 1000, pool, tags)
|
||||
var c = beginRead(buf)
|
||||
if c.kind != TagLit or c.cursorTagId != stmtsTag:
|
||||
endRead(c)
|
||||
return
|
||||
c.loopInto:
|
||||
if c.kind == TagLit and c.cursorTagId == loweredTag:
|
||||
c.loopInto:
|
||||
if c.kind == StrLit:
|
||||
result.add strVal(c)
|
||||
inc c
|
||||
else:
|
||||
skip c
|
||||
else:
|
||||
skip c
|
||||
endRead(c)
|
||||
|
||||
type
|
||||
CnifLiveness* = object
|
||||
defs*: int ## proc definitions emitted across all modules
|
||||
|
||||
@@ -51,7 +51,7 @@ proc parsedFile(c: DepContext; f: FilePair): string =
|
||||
getNimcacheDir(c.config).string / f.modname & ".p.nif"
|
||||
|
||||
proc semmedFile(c: DepContext; f: FilePair): string =
|
||||
getNimcacheDir(c.config).string / f.modname & ".nif"
|
||||
getNimcacheDir(c.config).string / f.modname & ".s.nif"
|
||||
|
||||
proc ifaceFile(c: DepContext; f: FilePair): string =
|
||||
## Interface-cookie sidecar written by `nim m` (ast2nif.writeIfaceCookie,
|
||||
@@ -941,9 +941,11 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
|
||||
# Per-node output paths.
|
||||
var cnifFiles = newSeq[string](c.nodes.len)
|
||||
var cFiles = newSeq[string](c.nodes.len)
|
||||
var tFiles = newSeq[string](c.nodes.len)
|
||||
for i, node in c.nodes:
|
||||
cFiles[i] = backendCFile(c, node)
|
||||
cnifFiles[i] = cFiles[i] & ".nif"
|
||||
tFiles[i] = cFiles[i] & ".t.nif"
|
||||
|
||||
var b = nifbuilder.open(result)
|
||||
defer: b.close()
|
||||
@@ -979,9 +981,28 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
|
||||
b.addStrLit s
|
||||
b.endTree()
|
||||
|
||||
# cg: one rule per module. Inputs are the project (slot 0) and every semmed
|
||||
# NIF (so the whole program loads and the rule is ordered after the frontend);
|
||||
# the main module additionally depends on every other `.c.nif` (init metas).
|
||||
# lower: one rule per module. Transforms (eventually) the routines the module
|
||||
# OWNS once, in the owner's id space, into `<module>.t.nif`, so the `cg` stage
|
||||
# reads them instead of re-deriving (which makes a closure `:env`'s identity
|
||||
# diverge across the parallel `cg` processes). Runs per module in parallel on
|
||||
# the shallow backend dep-graph. Inputs mirror `cg` (project + every semmed
|
||||
# NIF) so the rule is ordered after the frontend.
|
||||
for i, node in c.nodes:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:lower"
|
||||
b.addStrLit "--icBackendModule:" & node.files[0].modname
|
||||
inputStr mainNif
|
||||
for n2 in c.nodes:
|
||||
inputStr c.semmedFile(n2.files[0])
|
||||
outputStr tFiles[i]
|
||||
b.endTree()
|
||||
|
||||
# cg: one rule per module. Inputs are the project (slot 0), every semmed
|
||||
# NIF (so the whole program loads and the rule is ordered after the frontend)
|
||||
# and this module's `.t.nif` (its lowered bodies); the main module additionally
|
||||
# depends on every other `.c.nif` (init metas).
|
||||
for i, node in c.nodes:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
@@ -991,6 +1012,7 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
|
||||
inputStr mainNif
|
||||
for n2 in c.nodes:
|
||||
inputStr c.semmedFile(n2.files[0])
|
||||
inputStr tFiles[i]
|
||||
if node.id == 0:
|
||||
for j in 0 ..< c.nodes.len:
|
||||
if c.nodes[j].id != 0:
|
||||
|
||||
@@ -245,6 +245,18 @@ proc genOp(c: var Con; t: PType; kind: TTypeAttachedOp; dest, ri: PNode): PNode
|
||||
let canon = c.graph.canonTypes.getOrDefault(h)
|
||||
if canon != nil:
|
||||
op = getAttachedOp(c.graph, canon, kind)
|
||||
if (op == nil or op.ast.isGenericRoutine) and icLoweredBodies(c.graph.config):
|
||||
# IC: injectDestructorCalls is demand-driven and runs HERE (cg), not in the
|
||||
# `lower` stage, so a structural, env-agnostic op the lower stage never had
|
||||
# reason to serialize — most often a closure PROC type's `=destroy`/`=sink`
|
||||
# (which act on the `(ClP_0, ClE_0)` tuple, NOT the concrete env) — must be
|
||||
# lifted on demand, exactly as the lazy path's cg does. This is safe now:
|
||||
# closure-env identity resolves via `attachedOps[itemId]`/env-erased typeKey,
|
||||
# env objects load complete, and atomicRefOp's type-erased path covers any
|
||||
# still-incomplete env (so the lift never walks a nil field).
|
||||
excl t.flagsImpl, tfCheckedForDestructor
|
||||
createTypeBoundOps(c.graph, nil, t, dest.info, c.idgen)
|
||||
op = getAttachedOp(c.graph, t, kind)
|
||||
if op == nil:
|
||||
#echo dest.typ.id
|
||||
globalError(c.graph.config, dest.info, "internal error: '" & AttachedOpToStr[kind] &
|
||||
|
||||
@@ -176,7 +176,7 @@ proc closureParams(routine: PSym): PNode =
|
||||
result = routine.typ.n
|
||||
routine.ast[paramsPos] = result
|
||||
|
||||
proc addHiddenParam(routine: PSym, param: PSym) =
|
||||
proc addHiddenParam*(routine: PSym, param: PSym) =
|
||||
assert param.kind == skParam
|
||||
var params = closureParams(routine)
|
||||
# -1 is correct here as param.position is 0 based but we have at position 0
|
||||
|
||||
@@ -761,6 +761,28 @@ proc cyclicType*(g: ModuleGraph, t: PType): bool =
|
||||
of tyProc: result = t.callConv == ccClosure
|
||||
else: result = false
|
||||
|
||||
proc recHasNilFieldType(n: PNode): bool =
|
||||
case n.kind
|
||||
of nkSym: result = n.sym == nil or n.sym.typ == nil
|
||||
of nkRecList, nkRecCase:
|
||||
for ch in n:
|
||||
if recHasNilFieldType(ch): return true
|
||||
result = false
|
||||
else: result = false
|
||||
|
||||
proc isTypeErasedEnvRef(config: ConfigRef; elemType: PType): bool =
|
||||
## IC: a foreign closure-env object can load (cross-module) with nil-typed
|
||||
## derived fields when its hook key diverges across the NIF boundary. Per the
|
||||
## closure type-erasure principle, destroying such a `ref` must go through RTTI
|
||||
## (`nimDestroyAndDispose` / `nimDecRefIsLastCyclicDyn`), NEVER a statically
|
||||
## lifted concrete env destructor — the producer module emits that destructor
|
||||
## and registers it in the env object's type info. Detect the incomplete load
|
||||
## so `atomicRefOp` takes the dynamic-dispatch path and never walks the nil
|
||||
## field (which would SIGSEGV).
|
||||
if not config.icLoweredBodies: return false
|
||||
let obj = elemType.skipTypes({tyGenericInst, tyAlias, tySink, tyOwned})
|
||||
result = obj.kind == tyObject and obj.n != nil and recHasNilFieldType(obj.n)
|
||||
|
||||
proc atomicRefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
|
||||
#[ bug #15753 is really subtle. Usually the classical write barrier for reference
|
||||
counting looks like this::
|
||||
@@ -793,13 +815,17 @@ proc atomicRefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
|
||||
]#
|
||||
var actions = newNodeI(nkStmtList, c.info)
|
||||
let elemType = t.elementType
|
||||
# (b) type-erasure: a foreign closure env that loaded incomplete cross-module
|
||||
# must be destroyed via RTTI, not by lifting its concrete (nil-fielded) object.
|
||||
let erased = isTypeErasedEnvRef(c.g.config, elemType)
|
||||
|
||||
createTypeBoundOps(c.g, c.c, elemType, c.info, c.idgen)
|
||||
if not erased:
|
||||
createTypeBoundOps(c.g, c.c, elemType, c.info, c.idgen)
|
||||
|
||||
# YRC uses dedicated runtime procs for the entire write barrier:
|
||||
if c.g.config.selectedGC == gcYrc:
|
||||
let desc =
|
||||
if isFinal(elemType):
|
||||
if isFinal(elemType) and not erased:
|
||||
let ti = genBuiltin(c, mGetTypeInfoV2, "getTypeInfoV2", newNodeIT(nkType, x.info, elemType))
|
||||
ti.typ = getSysType(c.g, c.info, tyPointer)
|
||||
ti
|
||||
@@ -814,31 +840,40 @@ proc atomicRefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
|
||||
return
|
||||
else: discard # fall through for destructor, trace, wasMoved
|
||||
|
||||
let isCyclic = c.g.config.selectedGC in {gcOrc, gcYrc} and types.canFormAcycle(c.g, elemType)
|
||||
# `erased` must short-circuit BEFORE canFormAcycle/isPureObject/isFinal, which
|
||||
# walk the (nil-fielded) type graph. Assume cyclic + dynamic — the dyn runtime
|
||||
# calls work for any ref via its type info.
|
||||
let isCyclic = erased or
|
||||
(c.g.config.selectedGC in {gcOrc, gcYrc} and types.canFormAcycle(c.g, elemType))
|
||||
|
||||
let isInheritableAcyclicRef = c.g.config.selectedGC in {gcOrc, gcYrc} and
|
||||
let isInheritableAcyclicRef = (not erased) and c.g.config.selectedGC in {gcOrc, gcYrc} and
|
||||
(not isPureObject(elemType)) and
|
||||
tfAcyclic in skipTypes(elemType, abstractInst+{tyOwned}-{tyTypeDesc}).flags
|
||||
# dynamic Acyclic refs need to use dyn decRef
|
||||
|
||||
let useStatic = (not erased) and isFinal(elemType)
|
||||
|
||||
let tmp =
|
||||
if isCyclic and c.kind in {attachedAsgn, attachedSink, attachedDup}:
|
||||
declareTempOf(c, body, x)
|
||||
else:
|
||||
x
|
||||
|
||||
if isFinal(elemType):
|
||||
if useStatic:
|
||||
addDestructorCall(c, elemType, actions, genDeref(tmp, nkDerefExpr))
|
||||
var alignOf = genBuiltin(c, mAlignOf, "alignof", newNodeIT(nkType, c.info, elemType))
|
||||
alignOf.typ = getSysType(c.g, c.info, tyInt)
|
||||
actions.add callCodegenProc(c.g, "nimRawDispose", c.info, tmp, alignOf)
|
||||
else:
|
||||
addDestructorCall(c, elemType, newNodeI(nkStmtList, c.info), genDeref(tmp, nkDerefExpr))
|
||||
# `nimDestroyAndDispose` resolves the real destructor via the object's RTTI,
|
||||
# so the env destructor the producer emitted runs — no static lift needed.
|
||||
if not erased:
|
||||
addDestructorCall(c, elemType, newNodeI(nkStmtList, c.info), genDeref(tmp, nkDerefExpr))
|
||||
actions.add callCodegenProc(c.g, "nimDestroyAndDispose", c.info, tmp)
|
||||
|
||||
var cond: PNode
|
||||
if isCyclic:
|
||||
if isFinal(elemType):
|
||||
if useStatic:
|
||||
let typInfo = genBuiltin(c, mGetTypeInfoV2, "getTypeInfoV2", newNodeIT(nkType, x.info, elemType))
|
||||
typInfo.typ = getSysType(c.g, c.info, tyPointer)
|
||||
cond = callCodegenProc(c.g, "nimDecRefIsLastCyclicStatic", c.info, tmp, typInfo)
|
||||
@@ -873,7 +908,7 @@ proc atomicRefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
|
||||
of attachedDeepCopy: assert(false, "cannot happen")
|
||||
of attachedTrace:
|
||||
if isCyclic:
|
||||
if isFinal(elemType):
|
||||
if useStatic:
|
||||
let typInfo = genBuiltin(c, mGetTypeInfoV2, "getTypeInfoV2", newNodeIT(nkType, x.info, elemType))
|
||||
typInfo.typ = getSysType(c.g, c.info, tyPointer)
|
||||
body.add callCodegenProc(c.g, "nimTraceRef", c.info, genAddrOf(x, c.idgen), typInfo, y)
|
||||
|
||||
@@ -904,7 +904,7 @@ proc getBody*(g: ModuleGraph; s: PSym): PNode {.inline.} =
|
||||
assert result != nil
|
||||
|
||||
when not defined(nimKochBootstrap):
|
||||
proc registerLoadedHooks(g: ModuleGraph; logOps: seq[LogEntry]) =
|
||||
proc registerLoadedHooks*(g: ModuleGraph; logOps: seq[LogEntry]) =
|
||||
let mainSuffix = getMainModuleSuffix(ast.program)
|
||||
for x in logOps:
|
||||
# A dependency's NIF may carry hooks whose syms belong to the module we
|
||||
|
||||
@@ -26,6 +26,8 @@ import ast, options, lineinfos, modulegraphs, cgendata, cgen,
|
||||
pathutils, extccomp, msgs, modulepaths, idents, types, ast2nif, typekeys,
|
||||
cnif
|
||||
from cgmeth import generateIfMethodDispatchers
|
||||
from transf import transformBody
|
||||
from lambdalifting import getEnvParam, addHiddenParam, paramName
|
||||
import ic / replayer
|
||||
|
||||
proc loadModuleDependencies(g: ModuleGraph; mainFileIdx: FileIndex;
|
||||
@@ -145,6 +147,32 @@ proc signatureHasMetaType(t: PType; depth: int = 0): bool =
|
||||
for k in t.kids:
|
||||
if signatureHasMetaType(k, depth + 1): return true
|
||||
|
||||
proc ownsRuntimeRoutine(s: PSym; modPos: int): bool =
|
||||
## A concrete, non-generic, runtime routine with a real body, OWNED by the
|
||||
## module at `modPos`. Shared by the `cg` stage's owned-routine seeding (so a
|
||||
## routine called only from other modules is still emitted by somebody) and
|
||||
## the `lower` stage's owned-routine enumeration, so both stages see exactly
|
||||
## the same set. The exclusions:
|
||||
## - nested/closure procs (owner is a proc, not a module): emitted via their
|
||||
## enclosing routine's lambda-lifting, never standalone;
|
||||
## - generic instances (`sfFromGeneric`): emitted by demand, deduped by merge;
|
||||
## - `importc`/`compileTime`/`error`/forward sentinels and meta signatures:
|
||||
## not real codegen targets.
|
||||
## A `{.closure.}` iterator IS a standalone runtime routine (unlike an inline
|
||||
## iterator, which is expanded at each call site) and must be emitted by its
|
||||
## owner — else a cross-module `for` over it links to nothing.
|
||||
s.itemId.module == modPos and
|
||||
(s.kind in {skProc, skFunc, skConverter, skMethod} or
|
||||
(s.kind == skIterator and s.typ != nil and s.typ.callConv == ccClosure)) and
|
||||
s.skipGenericOwner != nil and s.skipGenericOwner.kind == skModule and
|
||||
s.magic == mNone and
|
||||
sfFromGeneric notin s.flags and
|
||||
{sfForward, sfImportc, sfCompileTime, sfError} * s.flags == {} and
|
||||
s.typ != nil and not signatureHasMetaType(s.typ) and
|
||||
s.ast != nil and s.ast.safeLen > bodyPos and
|
||||
s.ast[genericParamsPos].kind == nkEmpty and
|
||||
s.ast[bodyPos].kind != nkEmpty
|
||||
|
||||
proc generateCodeForModule(g: ModuleGraph; precomp: PrecompiledModule) =
|
||||
## Generate C code for a single module.
|
||||
let moduleId = precomp.module.position
|
||||
@@ -170,34 +198,7 @@ proc generateCodeForModule(g: ModuleGraph; precomp: PrecompiledModule) =
|
||||
if g.config.cmd == cmdNifC and g.config.icBackendStage == "cg":
|
||||
let modPos = precomp.module.position
|
||||
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
|
||||
if s.itemId.module == modPos and
|
||||
s.kind in {skProc, skFunc, skConverter, skMethod} and
|
||||
# Only MODULE-level routines: a nested/closure proc (its owner is a
|
||||
# proc) captures its enclosing scope and cannot be emitted standalone —
|
||||
# the captured params have no loc → `expr: param not init`. Nested procs
|
||||
# are emitted via their enclosing routine's lambda-lifting, so seeding
|
||||
# the enclosing (module-level) routine already covers them.
|
||||
s.skipGenericOwner != nil and s.skipGenericOwner.kind == skModule and
|
||||
s.magic == mNone and
|
||||
# Skip generic instances: they have no single owning-module top-level
|
||||
# and are emitted by demand (emit-everywhere, deduped by the merge
|
||||
# stage). An instance has an empty `genericParamsPos` just like a plain
|
||||
# concrete proc, so only `sfFromGeneric` tells them apart; seeding one
|
||||
# would force standalone codegen of an instance body whose `when T is X`
|
||||
# branches were never folded for this path → `genMagicExpr: mIs`.
|
||||
sfFromGeneric notin s.flags and
|
||||
# Every other routine the module owns must be emitted here, exported or
|
||||
# not: a non-exported helper is still reached from another module when a
|
||||
# `template`/inline routine expands at a call site there (e.g. msgs'
|
||||
# `internalErrorImpl` behind the `internalError` template), and that
|
||||
# caller now only prototypes it. `{.error.}`/`compileTime` sentinels and
|
||||
# bodyless forward decls are not real codegen targets.
|
||||
{sfForward, sfImportc, sfCompileTime, sfError} * s.flags == {} and
|
||||
s.typ != nil and not signatureHasMetaType(s.typ) and
|
||||
s.ast != nil and s.ast.safeLen > bodyPos and
|
||||
s.ast[genericParamsPos].kind == nkEmpty and
|
||||
s.ast[bodyPos].kind != nkEmpty:
|
||||
# a concrete, non-generic, runtime routine with a real body, owned here
|
||||
if ownsRuntimeRoutine(s, modPos):
|
||||
requestProcDef(bmod, s)
|
||||
|
||||
proc loadBackendModules(g: ModuleGraph; mainFileIdx: FileIndex):
|
||||
@@ -323,6 +324,218 @@ proc findTargetModule(g: ModuleGraph; modules: seq[PrecompiledModule];
|
||||
cachedModuleSuffix(g.config, FileIndex precompSys.module.position) == suffix:
|
||||
return precompSys
|
||||
|
||||
proc findHiddenEnvParam(n: PNode; owner: PSym): PSym =
|
||||
## Locate the hidden env param (`:envP`) that belongs to `owner` in its loaded
|
||||
## transformed body, so it can be re-welded into `owner`'s signature. Matching
|
||||
## by `owner` is essential: a body can ALSO reference a callee's `:envP` (a
|
||||
## closure call passes the callee env), so the first `:envP` in DFS order is
|
||||
## not necessarily this proc's own.
|
||||
if n == nil: return nil
|
||||
if n.kind == nkSym:
|
||||
if n.sym != nil and n.sym.kind == skParam and n.sym.name.s == paramName and
|
||||
n.sym.owner == owner:
|
||||
return n.sym
|
||||
else:
|
||||
for i in 0 ..< n.safeLen:
|
||||
let r = findHiddenEnvParam(n[i], owner)
|
||||
if r != nil: return r
|
||||
return nil
|
||||
|
||||
proc registerLoweredModule(g: ModuleGraph; m: PSym; applyBodies: bool) =
|
||||
## Load module `m`'s `<m>.t.nif` and register its `lower`-stage output: its
|
||||
## embedded index (so its closure-env `@bk` entities resolve) and its lifted
|
||||
## type-bound ops (so any cg that DESTROYS one of `m`'s closures finds the env
|
||||
## `=destroy` via getAttachedOp). For the cg TARGET (`applyBodies`), also set
|
||||
## each owned routine's `transformedBody` so cg's `transformBody` short-circuits
|
||||
## (transf.nim:1383) instead of re-deriving. `injectDestructorCalls` stays in cg.
|
||||
let modPos = m.position
|
||||
let bmod = BModuleList(g.backend).mods[modPos]
|
||||
if bmod == nil: return
|
||||
let artifact = getCFile(bmod).string & ".t.nif"
|
||||
if not fileExists(artifact): return
|
||||
let suffix = cachedModuleSuffix(g.config, FileIndex modPos)
|
||||
let (bodies, hooks) = loadLoweredBodies(ast.program, FileIndex modPos, suffix,
|
||||
artifact, loadBodies = applyBodies)
|
||||
registerLoadedHooks(g, hooks)
|
||||
if not applyBodies: return
|
||||
var byName = initTable[string, PSym]()
|
||||
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
|
||||
# Owned routines AND nested closure routines (the `:anonymous` procs the
|
||||
# lower stage emits as their own entries) — both are index-resolvable syms
|
||||
# of this module whose transformed body the lower stage authored.
|
||||
if s.kind in routineKinds and s.itemId.module == modPos:
|
||||
byName[globalName(s, g.config)] = s
|
||||
for (name, body) in bodies:
|
||||
let s = byName.getOrDefault(name)
|
||||
# `.s.nif` wins: only fill from `.t.nif` if sem did not already transform it.
|
||||
if s != nil and body != nil and s.transformedBody == nil:
|
||||
s.transformedBody = body
|
||||
# Lambda-lift in the lower stage gave this proc a hidden `:envP` env param
|
||||
# (a captured-var closure env), but cg loaded the PRE-lift signature from
|
||||
# `.s.nif`. The transformed body references that `@bk` `:envP`; re-weld it
|
||||
# into the proc's params so genProc assigns it a loc (else "param not
|
||||
# init"). `transformBody` short-circuits on the cached body, skipping the
|
||||
# lift that normally adds it. This applies to BOTH a true `ccClosure` proc
|
||||
# (env arrives via the closure ABI `ClE_0`, needs `tfCapturesEnv`) and a
|
||||
# plain nested `nimcall` proc that merely captures (env is a regular last
|
||||
# param). Match the env param by owner — a body can also reference a
|
||||
# callee's `:envP`.
|
||||
if s.typ != nil and getEnvParam(s) == nil:
|
||||
let ep = findHiddenEnvParam(body, s)
|
||||
if ep != nil:
|
||||
# From-source, `ast[paramsPos]` and `typ.n` are the SAME node, but a
|
||||
# NIF-loaded routine has two distinct param nodes. genProc reads
|
||||
# `typ.n`, so unify them first — else addHiddenParam appends to
|
||||
# `ast[paramsPos]` and the env param never reaches genProc's loc setup.
|
||||
if s.typ.n != nil:
|
||||
s.ast[paramsPos] = s.typ.n
|
||||
addHiddenParam(s, ep)
|
||||
# The lower stage's lambda-lift converts EVERY captured nested proc to a
|
||||
# closure (collectNestedClosureBodies only emits `ccClosure` entries),
|
||||
# and the serialized call sites use the closure ABI. cg loaded the
|
||||
# pre-lift signature, which for a proc only ever CALLED (never used as a
|
||||
# value) is still `nimcall`. Re-apply the lift's `ccClosure` +
|
||||
# `tfCapturesEnv` so closureSetup maps the env param to `ClE_0` and the
|
||||
# calls match.
|
||||
s.typ.callConv = ccClosure
|
||||
incl(s.typ, {tfCapturesEnv})
|
||||
|
||||
proc applyLoweredBodies(g: ModuleGraph; modules: seq[PrecompiledModule];
|
||||
precompSys: PrecompiledModule; target: PrecompiledModule) =
|
||||
## Register every loaded module's `.t.nif` (env entities + lifted hooks),
|
||||
## applying transformed bodies only for the cg target.
|
||||
if not icLoweredBodies(g.config): return # Stage 0 (lazy): nothing to apply
|
||||
if precompSys.module != nil:
|
||||
registerLoweredModule(g, precompSys.module, applyBodies = false)
|
||||
for m in modules:
|
||||
if m.module != nil:
|
||||
registerLoweredModule(g, m.module,
|
||||
applyBodies = (m.module.position == target.module.position))
|
||||
|
||||
proc collectNestedClosureBodies(g: ModuleGraph; idgen: IdGenerator; n: PNode;
|
||||
owner: PSym; seen: var IntSet;
|
||||
entries: var seq[tuple[name: string; body: PNode]]) =
|
||||
## A closure routine nested in `owner` (the `:anonymous` proc lambda-lifting
|
||||
## minted, plus any deeper nesting) gets its captured-var→env rewrite produced
|
||||
## as part of the OWNER's `transformBody`, but only the owner's body is emitted
|
||||
## as a `(lowered)` entry. The nested proc itself IS index-resolvable (it has a
|
||||
## `.s.nif` sdef from sem, with its PRE-lift body), so cg loads that and
|
||||
## re-derives — and the capture mapping is gone (it accesses `x` directly
|
||||
## instead of `ClE_0->x0`). Walk the transformed body and emit each nested
|
||||
## closure routine's transformed body as its OWN `(lowered)` entry so
|
||||
## applyLoweredBodies installs it and cg reuses it verbatim.
|
||||
if n == nil: return
|
||||
if n.kind == nkSym:
|
||||
let s = n.sym
|
||||
if s != nil and s.kind in routineKinds and s != owner and
|
||||
not seen.containsOrIncl(s.id):
|
||||
if s.ast != nil and getBody(g, s).kind != nkEmpty and
|
||||
s.typ != nil and s.typ.callConv == ccClosure:
|
||||
if s.transformedBody == nil:
|
||||
s.transformedBody = transformBody(g, idgen, s, {})
|
||||
entries.add (globalName(s, g.config), s.transformedBody)
|
||||
collectNestedClosureBodies(g, idgen, s.transformedBody, s, seen, entries)
|
||||
else:
|
||||
for i in 0 ..< n.safeLen:
|
||||
collectNestedClosureBodies(g, idgen, n[i], owner, seen, entries)
|
||||
|
||||
proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## Per-module backend lowering (`--icBackendStage:lower --icBackendModule:<suffix>`):
|
||||
## enumerate the routines this module OWNS and write them to `<module>.t.nif`.
|
||||
## Eventually this transforms each owned routine once, in the owner's id space,
|
||||
## so `cg` reads the result instead of re-deriving it (re-derivation per
|
||||
## parallel `cg` process is the root of the closure-`:env` identity drift).
|
||||
## Runs per module in parallel on the shallow backend dep-graph — NOT folded
|
||||
## into the dense, mostly-serial sem stage.
|
||||
##
|
||||
## gate `newSymNode`'s lazy-type marking to the backend (see astdef) — the
|
||||
## transform builds sym nodes off not-yet-typed stubs, exactly as the `cg`
|
||||
## stage does.
|
||||
nifcBackendActive = true
|
||||
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
|
||||
let targetIsMain = g.config.icBackendModule.len == 0 or
|
||||
g.config.icBackendModule == mainSuffix
|
||||
var modules: seq[PrecompiledModule]
|
||||
var precompSys: PrecompiledModule
|
||||
var target: PrecompiledModule
|
||||
if targetIsMain:
|
||||
var nifFiles: seq[string]
|
||||
(modules, precompSys, nifFiles) = loadBackendModules(g, mainFileIdx)
|
||||
if modules.len == 0:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"Cannot load NIF file for main module: " & toFullPath(g.config, mainFileIdx))
|
||||
return
|
||||
target = findTargetModule(g, modules, precompSys, g.config.icBackendModule)
|
||||
else:
|
||||
(modules, precompSys, target) = loadDepClosure(g, g.config.icBackendModule)
|
||||
if target.module == nil:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module lowering: module not found for suffix: " & g.config.icBackendModule)
|
||||
return
|
||||
let modPos = target.module.position
|
||||
let tb = BModuleList(g.backend).mods[modPos]
|
||||
if tb == nil:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module lowering: no backend module for suffix: " & g.config.icBackendModule)
|
||||
return
|
||||
let artifact = getCFile(tb).string & ".t.nif"
|
||||
if icLoweredBodies(g.config):
|
||||
# STAGE 1 (DEFAULT; `-d:icNoLowerBodies` opts out): transform every owned routine
|
||||
# ONCE in this single process's id space and serialize the results, so `cg`
|
||||
# reads them instead of re-deriving (the single-writer-per-owner that keeps
|
||||
# closure-`:env` identity stable). `transformBody` with flags {} mirrors the
|
||||
# cg call (cgen.nim:1409); we keep only its return value (it clears
|
||||
# `transformedBody` for non-cached procs). `injectDestructorCalls` is NOT run
|
||||
# — it stays in `cg` on the loaded body.
|
||||
var entries: seq[tuple[name: string; body: PNode]] = @[]
|
||||
# `transformBody`/lambda-lifting LIFTS the closure env's type-bound ops
|
||||
# (`=destroy` etc.) into `g.opsLog`; snapshot its length so we can serialize
|
||||
# exactly the ops THIS stage created (not those loaded from `.s.nif`).
|
||||
let opsLogStart = g.opsLog.len
|
||||
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
|
||||
if ownsRuntimeRoutine(s, modPos):
|
||||
# `.s.nif` wins: a routine already transformed during sem (CT eval /
|
||||
# macro / VM transform) carries its lowered body in the `.s.nif` slot —
|
||||
# don't re-transform it here, just leave its `.t.nif` entry empty.
|
||||
if s.transformedBody != nil: continue
|
||||
let tbody = transformBody(g, tb.idgen, s, {})
|
||||
entries.add (globalName(s, g.config), tbody)
|
||||
var seenNested = initIntSet()
|
||||
collectNestedClosureBodies(g, tb.idgen, tbody, s, seenNested, entries)
|
||||
# Collect the hooks this stage lifted, and transform each hook ROUTINE's body
|
||||
# too (it is itself lowered into NIFC). The hooks' `(sd)` + transformed body go
|
||||
# into the `.t.nif`; `cg` re-attaches them so `injectDestructorCalls` resolves
|
||||
# the loaded env's `=destroy`. Iterate to a fixpoint: a hook body can lift
|
||||
# further hooks (a field's `=destroy`).
|
||||
var hooks: seq[LogEntry] = @[]
|
||||
var i = opsLogStart
|
||||
while i < g.opsLog.len:
|
||||
let e = g.opsLog[i]
|
||||
if e.kind == HookEntry and e.sym != nil and e.sym.kind in routineKinds and
|
||||
e.sym.transformedBody == nil:
|
||||
hooks.add e
|
||||
# Transform the hook routine's body and cache it on the sym so
|
||||
# `writeSymDef` serializes it in the hook's `(sd)` transformed-body slot
|
||||
# (`transformBody {}` returns the body but does not cache it).
|
||||
e.sym.transformedBody = transformBody(g, tb.idgen, e.sym, {})
|
||||
inc i
|
||||
# Seal the index-loaded entities so their references in the bodies serialize
|
||||
# as SymUses (resolved via the module index in cg), not duplicate defs.
|
||||
sealLoadedBackendEntities(ast.program)
|
||||
serializeLoweredBodies(g.config, modPos.int32, entries, hooks, artifact)
|
||||
if isDefined(g.config, "icDceCheck"):
|
||||
stderr.writeLine "[icLower] " & extractFilename(artifact) & " " &
|
||||
$entries.len & " routines transformed, " & $hooks.len & " hooks"
|
||||
else:
|
||||
# DEFAULT (Stage 0, byte-neutral): record one empty-marker per owned routine.
|
||||
# `cg` derives the transformed body itself, so output is unchanged; this only
|
||||
# exercises the artifact + scheduling the transform-move builds on.
|
||||
var names: seq[string] = @[]
|
||||
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
|
||||
if ownsRuntimeRoutine(s, modPos):
|
||||
names.add globalName(s, g.config)
|
||||
writeLoweredArtifact(artifact, names)
|
||||
|
||||
proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## Per-module backend codegen (`--icBackendStage:cg --icBackendModule:<suffix>`):
|
||||
## generate C for the single module named by `icBackendModule` and write only
|
||||
@@ -365,6 +578,7 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
"per-module codegen: module not found for suffix: " & g.config.icBackendModule)
|
||||
return
|
||||
|
||||
applyLoweredBodies(g, modules, precompSys, target)
|
||||
generateCodeForModule(g, target)
|
||||
let bl = BModuleList(g.backend)
|
||||
# The main module also owns the whole-program method dispatchers + NimMain.
|
||||
@@ -502,7 +716,10 @@ proc generateLinkStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## Main entry point for NIF-based C code generation.
|
||||
## Traverses the module dependency graph and generates C code.
|
||||
if g.config.icBackendStage == "cg":
|
||||
if g.config.icBackendStage == "lower":
|
||||
generateLowerStage(g, mainFileIdx)
|
||||
return
|
||||
elif g.config.icBackendStage == "cg":
|
||||
generateCgStage(g, mainFileIdx)
|
||||
return
|
||||
elif g.config.icBackendStage == "merge":
|
||||
@@ -516,4 +733,4 @@ proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
return
|
||||
else:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"the per-module NIF backend requires --icBackendStage:cg|merge|emit|link")
|
||||
"the per-module NIF backend requires --icBackendStage:lower|cg|merge|emit|link")
|
||||
|
||||
@@ -29,7 +29,7 @@ const
|
||||
|
||||
nimEnableCovariance* = defined(nimEnableCovariance)
|
||||
|
||||
icFormatVersion* = "14"
|
||||
icFormatVersion* = "16"
|
||||
## Version of the IC cache format (the sem-NIF module layout written by
|
||||
## ast2nif.nim plus the iface/impl/edges side files). Bump it whenever
|
||||
## that layout changes: `commandIc` wipes a nimcache whose `ic.version`
|
||||
@@ -786,6 +786,13 @@ template quitOrRaise*(conf: ConfigRef, msg = "") =
|
||||
else:
|
||||
quit(msg) # quits with QuitFailure
|
||||
|
||||
proc icLoweredBodies*(conf: ConfigRef): bool {.inline.} =
|
||||
## Whether the `nim ic` backend uses the EAGER per-module `lower` stage
|
||||
## (transformBody serialized to `.t.nif`, cg reuses it) instead of the lazy
|
||||
## Stage-0 path (cg re-derives every transformed body). This is now the
|
||||
## DEFAULT; `-d:icNoLowerBodies` opts back into the lazy path for A/B testing.
|
||||
not isDefined(conf, "icNoLowerBodies")
|
||||
|
||||
proc importantComments*(conf: ConfigRef): bool {.inline.} = conf.cmd in cmdDocLike + {cmdIdeTools}
|
||||
proc usesWriteBarrier*(conf: ConfigRef): bool {.inline.} = conf.selectedGC >= gcRefc
|
||||
proc usesSso*(conf: ConfigRef): bool {.inline.} = conf.selectedStrings == stringSso
|
||||
|
||||
Reference in New Issue
Block a user