diff --git a/compiler/ast.nim b/compiler/ast.nim index 068b3c4f86..e2abffeb0e 100644 --- a/compiler/ast.nim +++ b/compiler/ast.nim @@ -824,6 +824,10 @@ proc newSymNode*(sym: PSym): PNode = result = newNode(nkSym) result.sym = sym result.typField = sym.typ + if result.typField == nil and nifcBackendActive: + # See the two-arg overload in astdef: in the NIF backend cg stage a sym node + # built from a not-yet-typed stub must track the symbol's type lazily. + result.flags.incl nfLazyType result.info = sym.info proc newOpenSym*(n: PNode): PNode {.inline.} = diff --git a/compiler/ast2nif.nim b/compiler/ast2nif.nim index 270f1c048e..b2525510d5 100644 --- a/compiler/ast2nif.nim +++ b/compiler/ast2nif.nim @@ -1571,6 +1571,14 @@ proc cursorFromIndexEntry(c: var DecodeContext; module: FileIndex; entry: NifInd buf: var TokenBuf): Cursor = let s = 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 + # depth >1 by a prior non-seek read (e.g. loadNifModule reads `(stmts`/ + # `(implementation` without consuming their `)`), and `parse` only overwrites + # parents[0] while `rawNext` reads parents[^1] — so a stale top entry (the last + # symbol decoded) would become the base, corrupting the decoded line info. + # Collapse the stack so parse's parentInfo is the sole base. + s[].parents.setLen 1 nifcursors.parse(s[], buf, entry.info) result = cursorAt(buf, 0) @@ -1813,7 +1821,20 @@ 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.getOffset(module, symAsStr) + let offs = c.mods[module].index.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 + # `include`d symbol, or a re-exported module qualifier left dangling in a + # dead `when`-branch (`inlineasm.arm64.x`). Synthesize a resolvable skModule + # stub (itemId item-0 = the module self-sym) instead of asserting "symbol + # has no offset". `Complete` so accessors never try to lazy-load it; a real + # `newLineInfo` so an owner/decl reference renders a path, not `???`. + result = PSym(itemId: itemId(module.int32, 0'i32), kindImpl: skModule, + name: c.cache.getIdent(sn.name), disamb: sn.count.int32, + infoImpl: newLineInfo(module, 1, 1), state: Complete) + c.syms[symAsStr] = (result, NifIndexEntry()) + return result let (stubKind, stubName) = stubKindAndName(c.cache, sn.name) result = PSym(itemId: id, kindImpl: stubKind, name: stubName, disamb: sn.count.int32, state: Partial) c.syms[symAsStr] = (result, offs) diff --git a/compiler/astdef.nim b/compiler/astdef.nim index 67f7b02fa5..19ad21f0af 100644 --- a/compiler/astdef.nim +++ b/compiler/astdef.nim @@ -17,6 +17,15 @@ when defined(nimPreviewSlimSystem): export int128 +var nifcBackendActive* = false + ## Set only while the per-module NIF backend codegen stage runs + ## (`nifbackend.generateCgStage`, `cmd == cmdNifC`). It gates `newSymNode`'s + ## lazy-type marking so it applies ONLY in the backend — where syms are loaded + ## from NIF and a cg-stage transform can build a sym node from a not-yet-typed + ## stub — and never during frontend sem, where the same marking would perturb + ## effect/exception inference (it diverges from a non-IC build, e.g. + ## `times.toDateTimeByWeek` gaining a spurious unlisted `Exception`). + import nodekinds export nodekinds @@ -970,6 +979,14 @@ proc newSymNode*(sym: PSym, info: TLineInfo): PNode = result = newNode(nkSym) result.sym = sym result.typField = sym.typImpl + if result.typField == nil and nifcBackendActive: + # In the per-module NIF backend cg stage a transform (chronos async + # closure-iterator lowering) builds `result = …` sym nodes from a not-yet-typed + # NIF stub; snapshotting the nil here would leave the node permanently typeless + # and the backend later reads `t.flags` off it and SIGSEGVs (injectdestructors + # hasDestructor). Mark it lazy so `typ` re-reads `sym.typ` once resolved. Gated + # on `nifcBackendActive` so frontend sem is untouched (see the flag's doc). + result.flags.incl nfLazyType result.info = info proc newStrNode*(kind: TNodeKind, strVal: string): PNode = diff --git a/compiler/nifbackend.nim b/compiler/nifbackend.nim index 4f7791fe15..ed2567d91c 100644 --- a/compiler/nifbackend.nim +++ b/compiler/nifbackend.nim @@ -336,6 +336,8 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) = ## module still loads everything (`loadBackendModules`) because NimMain's init ## list and the method dispatchers are whole-program; its `cg` runs essentially ## alone (every other `.c.nif` precedes it), so it does not contend for memory. + # gate `newSymNode`'s lazy-type marking to this stage only (see astdef) + nifcBackendActive = true let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx) let targetIsMain = g.config.icBackendModule.len == 0 or g.config.icBackendModule == mainSuffix diff --git a/compiler/options.nim b/compiler/options.nim index 02f840c07e..33f2ac57ab 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -29,7 +29,7 @@ const nimEnableCovariance* = defined(nimEnableCovariance) - icFormatVersion* = "10" + icFormatVersion* = "12" ## 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` diff --git a/koch.nim b/koch.nim index 9d5bdca765..58850e03dd 100644 --- a/koch.nim +++ b/koch.nim @@ -618,7 +618,8 @@ 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", "ttypeoffer", "ttransitiveoffer"] + "tconverterreexport", "ttypeoffer", "ttransitiveoffer", + "tmodsymref"] proc icTest(args: string) = temp("") diff --git a/tests/ic/mmodsymadd.nim b/tests/ic/mmodsymadd.nim new file mode 100644 index 0000000000..75a5e3fe51 --- /dev/null +++ b/tests/ic/mmodsymadd.nim @@ -0,0 +1,12 @@ +import mmodsymasm + +# The dead `else` branch qualifies a re-exported module (`mmodsymarm`) whose +# `foo` is gated out on this host: the module sym binds at template-definition +# but the member never resolves, so a dangling module-symbol reference survives +# into the serialized template body. Before the `ModMarker` fix this failed +# under `nim ic` with `symbol has no offset` when the consumer loaded this NIF. +template satAdd*(a, b: uint64): uint64 = + when not defined(mmodSymFakeArch): + mmodsymasm.mmodsymx86.foo(a, b) + else: + mmodsymasm.mmodsymarm.foo(a, b) diff --git a/tests/ic/mmodsymarm.nim b/tests/ic/mmodsymarm.nim new file mode 100644 index 0000000000..c8d05dba82 --- /dev/null +++ b/tests/ic/mmodsymarm.nim @@ -0,0 +1,5 @@ +# Gated out on every host (mmodSymFakeArch is never defined): `foo` does NOT +# exist here, so a qualified `…mmodsymarm.foo` cannot resolve to the proc and +# leaves the bare re-exported MODULE symbol dangling in the template body. +when defined(mmodSymFakeArch): + func foo*(a, b: uint64): uint64 = a + b diff --git a/tests/ic/mmodsymasm.nim b/tests/ic/mmodsymasm.nim new file mode 100644 index 0000000000..ea5f73f30d --- /dev/null +++ b/tests/ic/mmodsymasm.nim @@ -0,0 +1,2 @@ +import mmodsymx86, mmodsymarm +export mmodsymx86, mmodsymarm diff --git a/tests/ic/mmodsymx86.nim b/tests/ic/mmodsymx86.nim new file mode 100644 index 0000000000..891568d49a --- /dev/null +++ b/tests/ic/mmodsymx86.nim @@ -0,0 +1,3 @@ +# Live on every real host: provides `foo` so the template's taken branch resolves. +when not defined(mmodSymFakeArch): + func foo*(a, b: uint64): uint64 = a + b diff --git a/tests/ic/tmodsymref.nim b/tests/ic/tmodsymref.nim new file mode 100644 index 0000000000..607f6da513 --- /dev/null +++ b/tests/ic/tmodsymref.nim @@ -0,0 +1,12 @@ +discard """ +output: '''7''' +""" + +# Regression test: a cross-module MODULE-symbol reference left as a dangling +# qualifier in a template body (here `mmodsymasm.mmodsymarm.foo` in a dead +# `when`-branch, reached via re-export) must load under `nim ic` instead of +# raising `symbol has no offset`. Mirrors nim-intops' `inlineasm.arm64.X` in +# nimbus-eth2. See compiler/ast2nif.nim `ModMarker`. + +import mmodsymadd +echo satAdd(3'u64, 4'u64)