diff --git a/compiler/nifbackend.nim b/compiler/nifbackend.nim index b23f5d2291..5fe36a1314 100644 --- a/compiler/nifbackend.nim +++ b/compiler/nifbackend.nim @@ -198,8 +198,16 @@ proc ownsRuntimeRoutine(s: PSym; modPos: int): bool = {sfForward, sfImportc, sfCompileTime, sfError} * s.flags == {} and s.typ != nil and not signatureHasMetaType(s.typ) and s.ast != nil and s.ast.safeLen > bodyPos and - s.ast[genericParamsPos].kind == nkEmpty and - s.ast[bodyPos].kind != nkEmpty + s.ast[genericParamsPos].kind == nkEmpty + # NOTE: an `nkEmpty` body is NOT a disqualifier. A concrete, owned, non- + # forward/-importc/-magic routine whose body folds to nothing is still a real + # definition the owner must emit (`void f(void){}`), exactly as whole-program + # cgen does — else a cross-module caller links to nothing. This bites e.g. + # Nimbus' `extras.incInternalErrors`, a plain `proc` whose sole statement is a + # metrics-counter `.inc()` that the `metrics` library expands to a no-op when + # the importing tool (ncli) builds with `-u:metrics`; the body is then a bare + # `nkEmpty`, but `state_transition_epoch` still calls it. Forward declarations + # (the other empty-body case) carry `sfForward` and are excluded above. proc generateCodeForModule(g: ModuleGraph; precomp: PrecompiledModule) = ## Generate C code for a single module. diff --git a/tests/ic/memptycaller.nim b/tests/ic/memptycaller.nim new file mode 100644 index 0000000000..88c8eeefb1 --- /dev/null +++ b/tests/ic/memptycaller.nim @@ -0,0 +1,9 @@ +# Calls `emptyOwned` from a DIFFERENT module than the one that owns it, so the +# reference at link time must resolve to a definition the owning module emits. + +import memptyowned + +proc callEmpty*(cond: bool) = + if cond: + emptyOwned() + echo "called ", cond diff --git a/tests/ic/memptyowned.nim b/tests/ic/memptyowned.nim new file mode 100644 index 0000000000..a1938400ee --- /dev/null +++ b/tests/ic/memptyowned.nim @@ -0,0 +1,12 @@ +# Helper for `temptyowned`: exports a concrete proc whose body folds to +# nothing (an `nkEmpty` body, like Nimbus' `extras.incInternalErrors` when the +# metrics counter's `.inc()` expands to a no-op under `-u:metrics`). The proc is +# NOT called within its own module — only `memptycaller` references it — so the +# per-module backend's owned-routine seeding is the ONLY thing that can emit it. + +template maybe*(x: untyped) = + when false: + x + +proc emptyOwned*() = + maybe(echo "unreachable") diff --git a/tests/ic/temptyowned.nim b/tests/ic/temptyowned.nim new file mode 100644 index 0000000000..2d37ebe034 --- /dev/null +++ b/tests/ic/temptyowned.nim @@ -0,0 +1,18 @@ +discard """ +output: '''called false +done''' +""" + +# Regression test for the per-module IC backend dropping an owned routine whose +# body folds to `nkEmpty`. `memptyowned.emptyOwned` is a real, concrete, owned +# proc with an empty body; `memptycaller.callEmpty` references it across a module +# boundary. The owning module's `ownsRuntimeRoutine` seeding used to reject any +# routine with an `nkEmpty` body, so nobody emitted `emptyOwned` -> undefined +# reference at link (mirrors Nimbus `ncli` linking `extras.incInternalErrors`). +# Whole-program cgen always emits it as `void emptyOwned(void){}`; the per-module +# backend must too. + +import memptycaller + +callEmpty(false) +echo "done"