Files
Nim/tests/ic/tclosure_hooks.nim
araq cce17461de IC: three closure/environment fixes found by differential grinding
A closure's environment type — and the `=destroy`/`=copy`/`=sink` the compiler
lifts for it — is minted by the BACKEND and exists in no module's semmed NIF.
Grinding a corpus of closure programs against `nim c` as the oracle turned up
three ways the per-module backend gets those wrong. `tests/closure` is now green
under `--ic:on`, and `tests/iter` goes from 13 failures to 9.

* **The env hook is emitted by nobody.** `emitsBodyInThisModule` walks up to the
  outermost enclosing routine to pick the TU that emits a body. For the env
  `=destroy` of a generic closure iterator defined in one module and instantiated
  in another, that walk lands on the module of the ORIGINAL generic — a module
  that never sees the instance — so the routine was emitted nowhere
  (`undefined reference to eqdestroy__c485__…`). A backend-minted routine has no
  owning module NIF at all: it is written into the `.t.bif` of every module that
  references it, re-homed there with `@bk`. Every referencing TU emits it now and
  the merge stage keeps one, like any other content-addressed definition.

* **The `:up` link is stored without an increment.** `env.:up = enclosingEnv` has
  to go through `=copy` (with the cyclic incref) or the parent's refcount is one
  too low, and at teardown the two envs' mutually recursive `=destroy`s each
  believe they hold the last reference and recurse until the stack is gone — a
  SIGSEGV after the program's own output has already been printed
  (`tests/iter/tnestedclosures`, "Test 3"). Whether it becomes a `=copy` depends
  on the up-field type's hooks existing at injection time. Whole-program cgen got
  that for free: a LATER lifting pass creates them and it runs before any
  routine's injection. The per-module backend injects a routine right after
  lifting it (`lower`), long before the module's top level is transformed at all
  (`cg`) — so both up-field assignment sites create the ops themselves.

* **Two backend hooks collide on one C name.** A backend-minted symbol is named
  `_c<itemId.item>`, a PER-PROCESS counter — fine while "nifc lifts, emits and
  compiles them in one run", which is what `mangleProcNameExt` assumed. But
  `lower` mints the env hooks of nested routines and `cg` mints those of the
  module's top level, and both land in the same translation unit: two unrelated
  `=destroy`s became one C function. `mangleProcNameExt` already makes an
  exception for hooks whose `disamb` is content-derived (`HookDisambBit`);
  `toNifSymName` now mirrors it, so the content-derived value survives the round
  trip instead of being overwritten by the loader.

`InstanceDisambBit`/`HookDisambBit` move to `astdef` — `ast2nif` names symbols by
them and cannot import `modulegraphs`.

Two new metamorphic tests, both of which fail on the previous compiler:
`tclosure_hooks` (generic closure iterator instantiated across modules) and
`tclosure_nested_iter` (closure iterator nested in a closure iterator).

`icFormatVersion` 37 -> 38 for the hook naming. `koch bootic` reaches its
byte-identical fixed point; `tests/closure`, `tests/destructor` (3 known
`--newruntime` failures), `tests/cpp` and the classic categories are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:12:51 +02:00

102 lines
2.7 KiB
Nim

discard """
description: '''IC vs `nim c`: closure environments, their hooks and their owners'''
"""
#? metamorphic
# A closure's environment type — and the `=destroy`/`=copy` the compiler lifts
# for it — is minted by the BACKEND, during the `lower` stage, and exists in no
# module's semmed NIF. The per-module backend has to decide which translation
# unit emits such a routine, and the owner walk it uses lands on the module of
# the ORIGINAL generic: for a generic closure iterator defined in one module and
# instantiated in another, that is a module which never sees the instance, so the
# env's `=destroy` was emitted by nobody (`undefined reference to
# eqdestroy__c485__…`). Every referencing TU emits it now.
#
# The steps then move the captured state around, because the env's LAYOUT is what
# decides whether those hooks are trivial: a body-only edit that adds a capture
# changes the env type of a routine whose importers do not re-sem.
#!FILE clleaf.nim
type Ev* = proc (s: string): string {.closure.}
proc leafMaker*(tag: string): Ev =
var n = 0
proc outer(s: string): string =
proc inner(t: string): string =
inc n
tag & ":" & t & ":" & $n
inner(s)
result = outer
iterator leafIter*[T](xs: seq[T]): T {.closure.} =
for x in xs: yield x
#!FILE clmid.nim
import clleaf
proc midMaker*(tag: string): Ev =
let base = leafMaker(tag & "/mid")
var calls = 0
result = proc (s: string): string =
inc calls
base(s) & "#" & $calls
proc midIter*(): seq[string] =
# instantiates `leafIter[string]` HERE, not where it is defined
result = @[]
for x in leafIter(@["p", "q"]): result.add x
#!FILE main.nim
import clleaf, clmid
let t = midMaker("top")
echo t("Alpha")
echo t("Beta")
echo midIter()
# an instance only the main module has
var fs: seq[float] = @[]
for x in leafIter(@[1.5, 2.5]): fs.add x
echo fs
#!STEP
# body-only edit that GROWS the environment: a second captured local
#!FILE clleaf.nim
type Ev* = proc (s: string): string {.closure.}
proc leafMaker*(tag: string): Ev =
var n = 0
var seen: seq[string] = @[]
proc outer(s: string): string =
proc inner(t: string): string =
inc n
seen.add t
tag & ":" & t & ":" & $n & ":" & $seen.len
inner(s)
result = outer
iterator leafIter*[T](xs: seq[T]): T {.closure.} =
var i = 0
for x in xs:
inc i
yield x
#!STEP
# and shrink it again
#!FILE clleaf.nim
type Ev* = proc (s: string): string {.closure.}
proc leafMaker*(tag: string): Ev =
var n = 0
proc outer(s: string): string =
proc inner(t: string): string =
inc n
tag & ":" & t & ":" & $n
inner(s)
result = outer
iterator leafIter*[T](xs: seq[T]): T {.closure.} =
for x in xs: yield x
#!STEP