IC: type-offers because why not, there is no end in sight

This commit is contained in:
Araq
2026-06-16 18:31:21 +02:00
parent f5d9e7a207
commit 009f8776e8
5 changed files with 115 additions and 3 deletions

View File

@@ -609,6 +609,7 @@ var importTag = registerTag("import")
var implTag = registerTag("implementation")
var reexpModTag = registerTag("reexpmod")
var offerTag = registerTag("offer")
var typeOfferTag = registerTag("toffer")
proc registerNifAstTags*() =
## (Re)registers ast2nif's NIF tags explicitly. The top-level `registerTag`
@@ -640,6 +641,7 @@ proc registerNifAstTags*() =
implTag = registerTag("implementation")
reexpModTag = registerTag("reexpmod")
offerTag = registerTag("offer")
typeOfferTag = registerTag("toffer")
proc writeNode(w: var Writer; dest: var TokenBuf; n: PNode; forAst = false) =
if n == nil:
@@ -1256,6 +1258,7 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode;
genericOffers: seq[tuple[generic, inst: PSym;
concreteTypes: seq[PType];
genericParamsCount: int]] = @[];
typeOffers: seq[tuple[generic: PSym; inst: PType]] = @[];
resolvedImportDeps: seq[FileIndex] = @[]) =
var w = Writer(infos: LineInfoWriter(config: config), currentModule: thisModule)
var content = createTokenBuf(300)
@@ -1325,6 +1328,47 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode;
w.deps.addSymUse pool.syms.getOrIncl(typeToNifSym(ct, w.infos.config)), NoLineInfo
w.deps.addParRi
# Generic TYPE-instance OFFERS: the `tyGenericInst` types this module created
# (e.g. `HashArray[8192, Gwei]`). Non-IC keeps ONE such instance in the global
# `typeInstCache`, so a structural bound computed at the first instantiation
# site (e.g. an `array[…]` bound that depends on a `mixin`/`compiles()` whose
# resolution differs by import scope) is frozen and reused everywhere. A
# separate `nim m` process never repopulates `typeInstCache` from NIFs, so it
# re-instantiates in its own scope and can compute a DIFFERENT bound (the SSZ
# `dataPerChunk` divergence). The loader rebuilds `g.typeInstCache` from these
# so `semtypinst.searchInstTypes` hits and reuses the baked instance.
# Layout: (toffer <genericBodySym> <instType>).
for off in typeOffers:
# Carry the generic body sym and the instance type as STRING LITERALS, not
# SymUse tokens: `addSymUse` rewrites a same-module reference into the NIF
# "local form" (suffix stripped, resolved by the content loader against the
# module being read), but this offer lives in the `deps` header and is read
# by a CONSUMER with no such module context. The full names round-trip
# verbatim as strings and `createTypeStub`/`resolveHookSym` resolve them
# directly (cf. `loadImport`, which carries module suffixes the same way).
w.deps.addParLe typeOfferTag, NoLineInfo
w.deps.addStrLit w.toNifSymName(off.generic)
w.deps.addStrLit typeToNifSym(off.inst, w.infos.config)
w.deps.addParRi
# OWNER MUST EMIT: a type reachable only through an offered instance — the
# `concreteTypes` of an offered proc instance (e.g. chronicles `writeValue[T]`,
# where `T` is this module's own object type) or an offered generic type
# instance — may never be reached by the normal top-level serialization above.
# If this module OWNS such a type, force-emit its typedef so that a consumer
# which reuses the offer can resolve the cross-module SymUse to it. Without this
# the consumer writes `t<k>.<i>.<thisSuffix>` and the loader asserts
# `symbol has no offset`. `writeType` emits the def (and recurses into owned
# sons) only for an own, still-Complete type; an already-Sealed one is skipped.
for off in genericOffers:
for ct in off.concreteTypes:
if ct != nil and ct.uniqueId.module == w.currentModule and ct.state == Complete:
writeType(w, bottom, ct)
for off in typeOffers:
if off.inst != nil and off.inst.uniqueId.module == w.currentModule and
off.inst.state == Complete:
writeType(w, bottom, off.inst)
# the implTag is used to tell the loader that the
# bottom of the file is the implementation of the module:
content.addParLe implTag, NoLineInfo
@@ -2268,6 +2312,11 @@ type
## generic instances this module created; modulegraphs.nim rebuilds
## `procInstCache` from them so a consumer reuses the instance instead of
## re-instantiating it in its own (operator-blind) module scope.
typeOffers*: seq[tuple[generic: PSym; inst: PType]]
## generic TYPE instances this module created; modulegraphs.nim rebuilds
## `typeInstCache` from them so a consumer reuses the baked instance
## (e.g. a `mixin`/`compiles()`-dependent array bound) instead of
## re-instantiating it with a different bound in its own scope.
proc loadImport(c: var DecodeContext; s: var Stream; deps: var seq[ModuleSuffix]; tok: var PackedToken) =
tok = next(s) # skip `(import`
@@ -2445,6 +2494,35 @@ proc processTopLevel(c: var DecodeContext; s: var Stream; flags: set[LoadFlag];
t = next(s)
if ok and genSym != nil and instSym != nil:
result.genericOffers.add (genSym, instSym, cts, paramsCount)
elif t.tagId == typeOfferTag:
# (toffer "<genericBodySym>" "<instType>") — see the writer. The two
# full names arrive as string literals; intern them and resolve to a
# PSym/PType, then FULLY load the instance (its array bounds/fields) so
# `searchInstTypes` can match its params (a Partial stub has empty kids).
# Best-effort: a type that fails to resolve drops the offer.
t = next(s) # skip (toffer
var genName, instName = ""
var idx = 0
while t.kind != ParRi and t.kind != EofToken:
if t.kind == StringLit:
if idx == 0: genName = pool.strings[t.litId]
elif idx == 1: instName = pool.strings[t.litId]
inc idx
t = next(s)
if t.kind != ParRi:
raiseAssert "expected ParRi in toffer entry of module " & suffix
t = next(s)
if genName.len > 0 and instName.len > 0:
# Resolving/loading these entities lazily reads from the SAME stream we
# are iterating, moving its cursor — save/restore around it (cf. the
# export-list handling above).
let saved = offset(s.r)
let genSym = resolveHookSym(c, pool.syms.getOrIncl(genName))
let inst = tryCreateTypeStub(c, pool.syms.getOrIncl(instName))
if genSym != nil and inst != nil:
loadType(c, inst)
result.typeOffers.add (genSym, inst)
s.r.jumpTo(saved)
elif t.tagId == implTag:
cont = false
elif LoadFullAst in flags or t.tagId == letTag or t.tagId == varTag:

View File

@@ -957,6 +957,22 @@ when not defined(nimKochBootstrap):
if n.kind == nkReplayAction and n.len >= 1 and n[0].kind == nkStrLit and
n[0].strVal in ["put", "inc", "add", "incl"]:
g.transitiveReplayActions.add n
# Rebuild generic TYPE- and PROC-instance offers across the WHOLE closure,
# not just direct imports (`moduleFromNifFile`). An instance is frozen at
# the FIRST module to create it (in a scope where its body's symbols
# resolve unambiguously); a consumer many imports away must REUSE it rather
# than re-instantiate in its own scope, which may resolve a body symbol
# differently — a divergent `compiles()`-dependent array bound (SSZ
# `HashArray[8192, Gwei]`, type offer), or an ambiguous unqualified ident
# leaked from an unrelated import (`fromRaw` -> `SkRawPublicKeySize` from
# both `secp` and `secp256k1`, proc offer). Direct-only rebuild left the
# deep offer invisible when the clean instance lives a transitive hop away.
for off in precomp.typeOffers:
g.typeInstCache.mgetOrPut(off.generic.itemId, @[]).add off.inst
for off in precomp.genericOffers:
g.procInstCache.mgetOrPut(off.generic.itemId, @[]).add PInstantiation(
sym: off.inst, concreteTypes: off.concreteTypes,
genericParamsCount: off.genericParamsCount, compilesId: 0)
for d in precomp.deps: stack.add d
proc materializeReexportedModule(g: ModuleGraph; mname, msuffix: string): PSym =
@@ -1031,6 +1047,14 @@ when not defined(nimKochBootstrap):
sym: off.inst, concreteTypes: off.concreteTypes,
genericParamsCount: off.genericParamsCount, compilesId: 0)
# Rebuild `typeInstCache` from this module's generic TYPE-instance OFFERS so a
# consumer's `searchInstTypes` reuses the baked instance (e.g. an SSZ
# `HashArray` whose array bound depends on import-scope-sensitive `compiles()`)
# rather than re-instantiating it with a divergent bound — see ast2nif's
# `(toffer …)`. Keyed by the generic body sym's itemId, as `searchInstTypes`.
for off in result.typeOffers:
g.typeInstCache.mgetOrPut(off.generic.itemId, @[]).add off.inst
# Mark module as cached
g.cachedMods.incl fileIdx.int
g.hookClosure.incl fileIdx.int

View File

@@ -29,7 +29,7 @@ const
nimEnableCovariance* = defined(nimEnableCovariance)
icFormatVersion* = "6"
icFormatVersion* = "9"
## 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`

View File

@@ -289,6 +289,16 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
if not hasNil:
genericOffers.add (inst.sym.instantiatedFrom, inst.sym,
inst.concreteTypes, inst.genericParamsCount)
# Generic TYPE-instance OFFERS: every `tyGenericInst` THIS module created,
# so a consumer reuses its baked structure (array bounds etc.) rather than
# re-instantiating with a scope-divergent bound. See ast2nif.writeNifModule.
var typeOffers: seq[tuple[generic: PSym; inst: PType]] = @[]
for genItemId, instList in graph.typeInstCache:
for inst in instList:
if inst != nil and inst.uniqueId.module == module.position and
inst.kidsLen > 0 and inst[0] != nil and
inst[0].kind == tyGenericBody and inst[0].sym != nil:
typeOffers.add (inst[0].sym, inst)
# The module's REAL resolved direct imports (incl. macro/template-generated
# ones with no surviving syntactic node). Passed to writeNifModule so the
# NIF `deps` section is complete (the backend closure walk needs it), and
@@ -296,7 +306,7 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
let resolvedImportDeps = graph.importDeps.getOrDefault(module.position.FileIndex, @[])
writeNifModule(graph.config, module.position.int32, topLevelStmts, graph.opsLog,
replayActions, implDeps, reexportedModuleSyms(graph, module),
genericOffers, resolvedImportDeps)
genericOffers, typeOffers, resolvedImportDeps)
# The module's REAL direct imports (incl. macro-generated) for `nim ic`'s
# graph re-derivation; see ast2nif.writeSemDeps / semdata.addImportFileDep.
var semDepPaths: seq[string] = @[]

View File

@@ -618,7 +618,7 @@ proc runIcTestFile(inp: string) =
# which exercises the NIF import/load path the single-file tests do not.
const icSuite = ["thallo", "tconverter", "timp", "tmiscs", "tparseutils",
"tcompiletimeglobal", "tsighashstable", "tpureenum", "tgenericoffer",
"tconverterreexport"]
"tconverterreexport", "ttypeoffer", "ttransitiveoffer"]
proc icTest(args: string) =
temp("")