IC: fix fwd-decl dup, import-except deps, and field interface leak

- ast2nif.canonicalRoutine: collapse a forward-decl's discarded impl sym
  onto the surviving proto so it is not serialized twice (was an ambiguous
  overload in importers; fixes tnewlit).
- deps.nim: handle `import m except syms` (importexcept) in the dependency
  scanner so the build-order edge is not dropped (fixes strformat->strutils
  ordering).
- ast2nif.writeSymDef: object fields (skField) are no longer marked
  bare-importable (x) in the NIF index; an exported field name leaked into
  importer scope and shadowed a template's open symbol (type mismatch 'T').
  Together these fix tmacro8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Araq
2026-06-08 22:49:23 +02:00
parent 8047413632
commit 4e6e9beea8
2 changed files with 37 additions and 3 deletions

View File

@@ -314,7 +314,16 @@ proc writeLib(w: var Writer; dest: var TokenBuf; lib: PLib) =
proc writeSymDef(w: var Writer; dest: var TokenBuf; sym: PSym) =
dest.addParLe sdefTag, trLineInfo(w, sym.infoImpl)
dest.addSymDef pool.syms.getOrIncl(w.toNifSymName(sym)), NoLineInfo
if {sfExported, sfFromGeneric} * sym.flagsImpl == {sfExported}:
# The `x` marker means "importable as a bare identifier into an importer's
# scope". Object fields carry `sfExported` (so they are visible via `obj.field`
# across modules) but must NOT become bare-importable: otherwise an exported
# field name (e.g. `HSlice.a`, whose type is a generic param `T`) leaks into
# module scope and a template's open/mixin symbol of the same name resolves to
# the field instead of a local, producing "type mismatch: got 'T'". Fields are
# still indexed (for `obj.field` resolution via the loaded object type); they
# are merely not advertised as importable. `skEnumField` stays importable —
# enum values are legitimately usable as bare identifiers.
if sym.kindImpl != skField and {sfExported, sfFromGeneric} * sym.flagsImpl == {sfExported}:
dest.addIdent "x"
else:
dest.addDotToken
@@ -379,7 +388,29 @@ proc shouldWriteSymDef(w: var Writer; sym: PSym): bool {.inline.} =
return true # Normal case for global symbols
return false
proc canonicalRoutine(sym: PSym): PSym {.inline.} =
## A forward declaration and its implementation are merged in sem into a single
## surviving symbol (the prototype). The discarded impl symbol is orphaned but
## can still be reachable from the surviving routine's AST — e.g. as the `owner`
## of a `[T: tuple]`-style generic-param constraint type created while the impl
## header was being processed. If we serialized that dead symbol it would get
## its own module-global sdef, be indexed, and the importer would then see two
## identical overloads -> "ambiguous call". The dead symbol is recognisable
## because its `ast` is the shared routine node whose name no longer points back
## at it (it points at the survivor). Collapse to the survivor. Writer-only /
## IC-specific: from-source compilation never serialises, so this cannot affect
## the non-IC path.
result = sym
if sym != nil and sym.kindImpl in routineKinds:
let a = sym.astImpl
if a != nil and a.len > namePos and a[namePos].kind == nkSym:
let canon = a[namePos].sym
if canon != nil and canon != sym and canon.name.id == sym.name.id and
canon.itemId.module == sym.itemId.module:
result = canon
proc writeSym(w: var Writer; dest: var TokenBuf; sym: PSym) =
let sym = canonicalRoutine(sym)
if sym == nil:
dest.addDotToken()
elif shouldWriteSymDef(w, sym):

View File

@@ -368,7 +368,7 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) =
if t.kind == ParLe:
let tag = pool.tags[t.tagId]
case tag
of "import", "fromimport", "include":
of "import", "fromimport", "importexcept", "include":
# Read first child. May be a `(when COND...)` marker — parse and
# evaluate; if the condition is statically false, skip the import
# entirely. Otherwise advance past the marker and parse the path.
@@ -395,7 +395,10 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) =
# that expand to several imports. A plain `import a, b, c` lists several
# modules as siblings; a `fromimport` has a single path followed by the
# imported symbol list, which must not be treated as modules.
if tag == "fromimport":
if tag == "fromimport" or tag == "importexcept":
# `from m import syms` / `import m except syms`: the first child is the
# module path; the rest is the (in/ex)cluded symbol list, which must not
# be treated as modules. Both still create a real dependency on `m`.
for importPath in parseImportPath(s, t):
if importPath.len > 0:
processImport(c, importPath, current)