diff --git a/compiler/sighashes.nim b/compiler/sighashes.nim index 67607b31ed..fdb089d7f4 100644 --- a/compiler/sighashes.nim +++ b/compiler/sighashes.nim @@ -149,6 +149,17 @@ proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]; conf: Confi # as properties are accessed and trigger lazy loading. backendEnsureMutable(t) + # Bare type-class keywords used as a typedesc without arguments (e.g. `array`, + # `range`, `distinct` passed to `signatureHash`) have no children, so the + # structural branches below would index a non-existent `elementType`. Hash them + # by kind (+ sym for an extra, stable distinction) — enough for a stable, + # distinct identity. (`seq`/`openArray`/`tuple` already fall through the empty + # `else` loop unharmed; this covers the branches that index `elementType`.) + if t.kind in {tyArray, tyRange, tyDistinct} and not t.hasElementType: + c &= char(t.kind) + if t.sym != nil: c.hashSym(t.sym) + return + case t.kind of tyGenericInvocation: for a in t.kids: diff --git a/compiler/vm.nim b/compiler/vm.nim index a2baa5ec3c..851ba1d9db 100644 --- a/compiler/vm.nim +++ b/compiler/vm.nim @@ -1955,7 +1955,21 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg = if regs[rb].node.kind != nkSym: stackTrace(c, tos, pc, "node is not a symbol") else: - regs[ra].node.strVal = $sigHash(regs[rb].node.sym, c.config) + let shSym = regs[rb].node.sym + # When `signatureHash` is applied to a type (e.g. a `T: typedesc`/generic + # param), hash the *type* it denotes, not the parameter symbol. Hashing the + # symbol routes through `hashNonProc`, which mixes in `s.disamb` — a + # per-module instantiation counter. Under incremental compilation the + # registering module and a consuming module instantiate the surrounding + # generic separately, get different `disamb`s, and produce different + # hashes for the same type (nim-serialization's auto-serialization lookup + # missed because of this). Hashing the underlying type via `hashType` is + # type-identity based and stable across the NIF boundary. + let shTyp = shSym.typ + if shTyp != nil and shTyp.kind == tyTypeDesc and shTyp.hasElementType: + regs[ra].node.strVal = $hashType(shTyp.elementType, c.config) + else: + regs[ra].node.strVal = $sigHash(shSym, c.config) of opcSlurp: decodeB(rkNode) createStr regs[ra] diff --git a/koch.nim b/koch.nim index e5909ea388..da43193469 100644 --- a/koch.nim +++ b/koch.nim @@ -617,7 +617,7 @@ proc runIcTestFile(inp: string) = # on a sibling helper (`timp` -> `myimp`, `tcompiletimeglobal` -> `mctglobal`), # which exercises the NIF import/load path the single-file tests do not. const icSuite = ["thallo", "tconverter", "timp", "tmiscs", "tparseutils", - "tcompiletimeglobal"] + "tcompiletimeglobal", "tsighashstable"] proc icTest(args: string) = temp("") diff --git a/tests/ic/msighashstable.nim b/tests/ic/msighashstable.nim new file mode 100644 index 0000000000..d5ce85d18a --- /dev/null +++ b/tests/ic/msighashstable.nim @@ -0,0 +1,48 @@ +# Helper module for tsighashstable.nim (not a test itself; no `discard`). +# +# Models nim-serialization's auto-serialization registry: a flavor records which +# types it auto-serializes in a `std/macrocache` keyed by `signatureHash(T)`, +# computed through a generic `{.compileTime.}` func. The registration happens +# here (at this module's compile time); the lookup happens in the importer. +# +# Under `nim ic` the two modules are compiled separately, so the generic +# `getSig[T]` is instantiated independently on each side. `signatureHash` must +# therefore hash the *type* `T` denotes, not the generic parameter symbol — the +# latter mixes in a per-module `disamb` counter and diverges across the NIF +# boundary, making the lookup miss. + +import std/[macrocache, macros, typetraits] + +type DefaultFlavor* = object + +macro calcSig*(T: typed): untyped = + doAssert(T.typeKind == ntyTypeDesc) + result = newLit(signatureHash(T)) + +func getSig*(F: type DefaultFlavor, T: distinct type): string {.compileTime.} = + calcSig(T) + +func getTable*(F: type DefaultFlavor): CacheTable {.compileTime.} = + CacheTable("nsrzStableTable" & typetraits.name(F)) + +func setAuto*(F: type DefaultFlavor, T: distinct type) {.compileTime.} = + let sig = F.getSig(T) + let table = F.getTable() + if not table.hasKey(sig): + table[sig] = newLit(1) + +func getAuto*(F: type DefaultFlavor, T: distinct type): bool {.compileTime.} = + let sig = F.getSig(T) + let table = F.getTable() + table.hasKey(sig) + +template autoCheck*(F: distinct type, T: distinct type, body) = + when not F.getAuto(T): + {.error: "auto serialization not enabled for `" & typetraits.name(T) & "`".} + else: + body + +static: + setAuto(DefaultFlavor, string) + setAuto(DefaultFlavor, SomeInteger) + setAuto(DefaultFlavor, seq) diff --git a/tests/ic/tsighashstable.nim b/tests/ic/tsighashstable.nim new file mode 100644 index 0000000000..11c20240d9 --- /dev/null +++ b/tests/ic/tsighashstable.nim @@ -0,0 +1,33 @@ +discard """ +output: '''ok string +ok int +ok seq''' +""" + +# Regression test: `signatureHash(T)` must be stable across the NIF boundary so +# that a macrocache keyed by it (nim-serialization's auto-serialization registry) +# can be populated in one module and queried from another under `nim ic`. +# +# Before the fix, `signatureHash` hashed the generic *parameter symbol* (whose +# `disamb` is a per-module instantiation counter) instead of the type it denotes. +# The registering module (msighashstable) and this importer instantiated the +# generic `getSig[T]` separately, got different `disamb`s, and the lookups for +# `string`/`SomeInteger` missed -> `{.error: auto serialization not enabled.}`. + +import msighashstable + +proc writeStr(F: type DefaultFlavor, v: string) = + autoCheck(F, string): + echo "ok string" + +proc writeInt[T: SomeInteger](F: type DefaultFlavor, v: T) = + autoCheck(F, SomeInteger): + echo "ok int" + +proc writeSeq[T](F: type DefaultFlavor, v: seq[T]) = + autoCheck(F, seq): + echo "ok seq" + +writeStr(DefaultFlavor, "hi") +writeInt(DefaultFlavor, 42) +writeSeq(DefaultFlavor, @[1, 2, 3])