mirror of
https://github.com/nim-lang/Nim.git
synced 2026-09-02 20:03:45 +00:00
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>
91 lines
2.5 KiB
Nim
91 lines
2.5 KiB
Nim
discard """
|
|
description: '''IC vs `nim c`: a closure iterator nested in a closure iterator'''
|
|
"""
|
|
|
|
#? metamorphic
|
|
|
|
# `env.:up = enclosingEnv` links a nested routine's environment to its parent,
|
|
# and the two environments then reference each other. That assignment has to go
|
|
# through `=copy` (with the cyclic increment) or the parent's refcount is one too
|
|
# low, and at teardown both `=destroy`s 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.nim`, "Test 3".)
|
|
#
|
|
# Whether it becomes a `=copy` depends on the up-field type's hooks existing when
|
|
# the routine is destructor-injected. 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 (the `lower`
|
|
# stage), long before the module's top level is transformed at all (that is
|
|
# `cg`) — so the hooks are created at the assignment site now.
|
|
|
|
#!FILE main.nim
|
|
iterator foo(): int {.closure.} =
|
|
let x = 34
|
|
proc bar() = echo "bar sees ", x
|
|
iterator bar2(): int {.closure.} =
|
|
bar()
|
|
yield x
|
|
for y in bar2():
|
|
yield y
|
|
|
|
for v in foo(): echo v
|
|
|
|
# a closure iterator nested in a closure iterator, inside a proc
|
|
proc factory() =
|
|
iterator outerIt(): int {.closure.} =
|
|
iterator innerIt(): int {.closure.} =
|
|
yield 0
|
|
yield 1
|
|
yield 2
|
|
for x in innerIt(): yield x
|
|
for x in outerIt(): echo x
|
|
factory()
|
|
|
|
# the iterator's env outlives the proc that made it
|
|
proc keep(): iterator (): string =
|
|
let held = "kept"
|
|
result = iterator (): string =
|
|
yield held
|
|
yield held & "!"
|
|
for s in keep()(): echo s
|
|
#!STEP
|
|
|
|
# growing the captured state changes both env layouts
|
|
#!FILE main.nim
|
|
iterator foo(): int {.closure.} =
|
|
let x = 34
|
|
var log: seq[string] = @[]
|
|
proc bar() =
|
|
log.add "bar"
|
|
echo "bar sees ", x, " ", log.len
|
|
iterator bar2(): int {.closure.} =
|
|
bar()
|
|
bar()
|
|
yield x
|
|
for y in bar2():
|
|
yield y
|
|
|
|
for v in foo(): echo v
|
|
|
|
proc factory() =
|
|
iterator outerIt(): int {.closure.} =
|
|
var emitted = 0
|
|
iterator innerIt(): int {.closure.} =
|
|
yield 0
|
|
yield 1
|
|
yield 2
|
|
for x in innerIt():
|
|
inc emitted
|
|
yield x * emitted
|
|
for x in outerIt(): echo x
|
|
factory()
|
|
|
|
proc keep(): iterator (): string =
|
|
let held = "kept"
|
|
let extra = "+"
|
|
result = iterator (): string =
|
|
yield held & extra
|
|
yield held & "!" & extra
|
|
for s in keep()(): echo s
|
|
#!STEP
|