From bff0bc45fd70d8bd6c6338f4a5e07ad2e1455020 Mon Sep 17 00:00:00 2001 From: araq Date: Mon, 31 Aug 2026 14:51:25 +0200 Subject: [PATCH] IC: `export s` makes s importable even without a `*` on its declaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `nim c --ic:on` could not compile anything that reached `std/tempfiles`: lib/std/tempfiles.nim(115, 38) Error: type mismatch Expression: initRand() Expected one of: [1] proc initRand(seed: int64) That is `std/random`. It declares `proc initRand(): Rand` WITHOUT a `*` and then, forty lines later, `since (1, 5, 1): export initRand`. Two mechanisms make a symbol importable and the writer only knew one. `sfExported` is the `*` on the declaration; `semExport` instead calls `reexportSym`, which adds the symbol to the module's interface table and sets no flag. `writeSymDef` decided the NIF `x` marker — "importable as a bare identifier" — from `sfExported` alone, so a symbol exported the second way shipped as private and its importer reported an undeclared identifier. Nothing about this is IC-specific in the source; it is ordinary stdlib code that classic compilation accepts. So ask the interface too: `reexportedLocalSyms` collects the symbols a module defines that reached its interface without the flag, and they get the marker. Symbols that are genuinely private still do not — the regression test's sibling case (`proc g()`, never exported) is still rejected under both `--ic:on` and `--ic:off`. This was found by pointing `--ic:on` at Atlas, which is now the first sizeable third-party program it builds. `nim c --ic:off` and `--ic:on` produce the same working binary from the same 209-module closure. ic 41/41 (the new test is the 41st), `koch boot -d:release` equal executables. --- compiler/ast2nif.nim | 14 ++++++++++++-- compiler/modulegraphs.nim | 17 +++++++++++++++++ compiler/pipelines.nim | 3 ++- tests/ic/mexportprivate.nim | 4 ++++ tests/ic/texportprivate.nim | 14 ++++++++++++++ 5 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 tests/ic/mexportprivate.nim create mode 100644 tests/ic/texportprivate.nim diff --git a/compiler/ast2nif.nim b/compiler/ast2nif.nim index 1b33da0769..421a412b4b 100644 --- a/compiler/ast2nif.nim +++ b/compiler/ast2nif.nim @@ -277,6 +277,9 @@ type 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 emittedCanonTypes: Table[string, int32] # canonical type name -> itemId.item of the def + extraExports: HashSet[ItemId] # symbols made importable by an explicit `export s` + # rather than by a `*` on the declaration; see + # `modulegraphs.reexportedLocalSyms` when defined(icLocalSymStats): @@ -1106,8 +1109,13 @@ proc writeSymDef(w: var Writer; dest: var IcBuilder; sym: PSym) = # ("undeclared field 'Number'"). let isPureEnumField = sym.kindImpl == skEnumField and sym.typImpl != nil and sym.typImpl.symImpl != nil and sfPure in sym.typImpl.symImpl.flagsImpl + # `sfExported` is the declaration's `*`. An explicit `export s` makes a symbol + # importable WITHOUT it (semExport -> reexportSym -> the interface table only), + # so ask the interface as well or those symbols ship as non-importable and the + # importer reports "undeclared identifier". if sym.kindImpl != skField and not isPureEnumField and - {sfExported, sfFromGeneric} * sym.flagsImpl == {sfExported}: + ({sfExported, sfFromGeneric} * sym.flagsImpl == {sfExported} or + sym.itemId in w.extraExports): dest.addIdent "x" else: dest.addDotToken @@ -2188,8 +2196,10 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode; resolvedImportDeps: seq[FileIndex] = @[]; firstUnusedId: int32 = 0; expansions: seq[(PSym, TLineInfo)] = @[]; - moduleFlags: int32 = 0) = + moduleFlags: int32 = 0; + extraExports: seq[ItemId] = @[]) = var w = Writer(infos: newLineInfoWriter(config), currentModule: thisModule) + for id in extraExports: w.extraExports.incl id w.deps = newIcBuilder(64) var content = newIcBuilder(300) diff --git a/compiler/modulegraphs.nim b/compiler/modulegraphs.nim index 64b21c60aa..c26a860099 100644 --- a/compiler/modulegraphs.nim +++ b/compiler/modulegraphs.nim @@ -315,6 +315,23 @@ proc reexportedModuleSyms*(g: ModuleGraph; m: PSym): seq[(string, string)] = not seen.containsOrIncl(s.position): result.add (s.name.s, cachedModuleSuffix(g.config, FileIndex s.position)) +proc reexportedLocalSyms*(g: ModuleGraph; m: PSym): seq[ItemId] = + ## Symbols DEFINED in `m` that reached `m`'s interface through an explicit + ## `export s` rather than through a `*` marker on their declaration. + ## + ## `semExport` re-exports by `reexportSym`, which adds to the interface table + ## and does NOT set `sfExported` — so a symbol can be importable while its + ## declaration says otherwise. The NIF writer decides importability from + ## `sfExported` alone and therefore missed exactly these. `std/random` does it + ## (`proc initRand(): Rand` private, then `since (1, 5, 1): export initRand`), + ## which is why `--ic:on` could not compile anything that reached + ## `std/tempfiles` — `initRand()` was undeclared in the importer. + result = @[] + for s in g.ifaces[m.position].interf.data: + if s != nil and s.kind != skModule and sfExported notin s.flags and + s.itemId.module == m.position: + result.add s.itemId + proc someSym*(g: ModuleGraph; m: PSym; name: PIdent): PSym = let importHidden = optImportHidden in m.options result = strTableGet(g.ifaces[m.position].interfSelect(importHidden), name) diff --git a/compiler/pipelines.nim b/compiler/pipelines.nim index f636ada22d..0837355467 100644 --- a/compiler/pipelines.nim +++ b/compiler/pipelines.nim @@ -339,7 +339,8 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator writeNifModule(graph.config, module.position.int32, topLevelStmts, graph.opsLog, replayActions, implDeps, reexportedModuleSyms(graph, module), genericOffers, typeOffers, resolvedImportDeps, firstUnusedId, - expansions, moduleFlags) + expansions, moduleFlags, + reexportedLocalSyms(graph, module)) # 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] = @[] diff --git a/tests/ic/mexportprivate.nim b/tests/ic/mexportprivate.nim new file mode 100644 index 0000000000..21f07e6193 --- /dev/null +++ b/tests/ic/mexportprivate.nim @@ -0,0 +1,4 @@ +proc pub*(x: int): int = x + 1 + +proc hidden(): int = 42 # no `*` ... +export hidden # ... but explicitly re-exported diff --git a/tests/ic/texportprivate.nim b/tests/ic/texportprivate.nim new file mode 100644 index 0000000000..8851cf14f8 --- /dev/null +++ b/tests/ic/texportprivate.nim @@ -0,0 +1,14 @@ +discard """ +output: '''42''' +""" + +# `export s` re-exports a symbol whose declaration has no `*`. It reaches the +# module interface through `reexportSym` alone, so a NIF writer that decides +# importability from `sfExported` ships it as private and the importer reports +# "undeclared identifier". `std/random` does exactly this +# (`proc initRand(): Rand` + `since (1, 5, 1): export initRand`), which made +# `--ic:on` unable to compile anything reaching `std/tempfiles`. + +import mexportprivate + +echo hidden()