Files
Nim/tests/ic/tdestructor_fidelity.nim
Andreas Rumpf c87926dadf IC: more bugfixes (#26141)
Grinding a small figdraw-based program under `nim ic` and diffing its
output against the classic backend surfaced eight bugs, four of which
silently produced a wrong binary rather than an error.

Frontend / build graph (`deps.nim`):

* Dead `when`-guarded imports were compiled anyway. `when someStrdefine
== "x": import y` is `cvUnknown` to the scanner, which conservatively
keeps the edge — right for an edge, but it also gave `y` its own `nim m`
rule, so a build died on a package the user never installed because they
never selected that backend. Track which edges are speculative and drop
a speculative subtree that cannot compile; if the guard was in fact
live, the discovery fixpoint puts the node back with the honest `cannot
open file`.
* Deleting a still-imported module went unnoticed: no mtime moves, so
nothing re-fires and `nim ic` relinked a stale binary while `nim c`
reported `cannot open file`. Report an unresolvable import from a
non-speculatively reached module during the graph scan.
* Macro-generated imports were discovered once and then forgotten.
Discovery only ran after a failure and the graph is re-derived
statically every run, so on a warm build the discovered module had no
rules at all and editing it changed nothing. Seed the graph from the
`.s.deps` sidecars up front.
* Config changes invalidated nothing. nifmake decides staleness from
file mtimes and never looks at a rule's command line, so `-d:foo=bar` /
`--mm:` / `--threads:` regenerated the build file with the new switches
and re-fired zero rules. Reify the configuration as a file and make it
an input of every rule.
* Command-line switches never reached the children: they replay the
project's config files, never the driver's argv, so `nim ic --opt:speed`
produced a byte-identical debug binary (likewise `--panics`,
`--experimental`, `--passC`). Forward the driver's switches, minus the
ones that must differ per child.

Artifacts and codegen:

* A failed `nim m` still wrote its `.s.bif` and cookies, so nifmake saw
the rule as satisfied on the next run: `nim ic` then reported success
for a program that does not compile, and generated code from
error-bearing AST (or hit an internal error in `ccgexprs`). Never
persist an artifact when `errorCounter > 0`.
* Top-level destructors were never injected. `sfInjectDestructors` lives
on the module symbol, which `moduleFromNifFile` rebuilds from scratch,
so `genTopLevelStmt` skipped `injectDestructorCalls` entirely: a
module-level `block: let h = openHandle()` never ran `=destroy`. Persist
the flag as a `(modflags)` record. `injectdestructors` also has to
tolerate the `nkReplayAction` entries the loader prepends to `topLevel`.
* `nfFirstWrite` / `nfLastRead` were dropped by the serializer. A sym
node is written as a bare NIF `SymUse` token, which has nowhere to put
node flags, so the frontend's move analysis never reached the backend:
EVERY first assignment to a destructor-bearing local compiled as
`=sink`, i.e. `=destroy` on still-zeroed memory followed by a copy, and
no read was ever a move. Wrap a sym use in `(nflags ...)` when it
carries persistent node flags.
2026-08-27 19:35:11 +02:00

69 lines
1.9 KiB
Nim

discard """
description: '''IC vs `nim c`: destructor injection and move analysis must agree'''
"""
#? metamorphic
# Two whole classes of IC miscompilation are invisible to any IC-vs-IC check,
# because IC was *consistently* wrong: warm == cold == not what `nim c` does.
# The oracle is what catches them.
#
# * `sfInjectDestructors` lives on the MODULE symbol, which the NIF loader
# rebuilds from scratch — so `genTopLevelStmt` skipped the destructor pass
# entirely and a module-level `block: let h = ...` never ran `=destroy`.
# * `nfFirstWrite`/`nfLastRead` sit on `nkSym` nodes, which serialize as bare
# NIF `SymUse` tokens with nowhere to put node flags — so the frontend's move
# analysis never reached the backend and EVERY first assignment to a
# destructor-bearing local became `=sink` over still-zeroed memory.
#!FILE res.nim
var log*: seq[string]
type R* = object
tag*: string
proc `=destroy`*(r: R) = log.add "d(" & r.tag & ")"
proc `=copy`*(d: var R, s: R) = (log.add "c(" & s.tag & ")"; d.tag = s.tag)
proc mk*(t: string): R = R(tag: t)
proc mkVia*(t: string): R = (result = R(tag: t))
proc consume*(r: sink R): string = "u:" & r.tag
#!FILE main.nim
import res
# in a proc: worked before
proc inProc() =
let a = mk("proc")
discard a
inProc()
# module top level: the pass was skipped wholesale
block:
let t = mk("toplevel")
discard t
for i in 0 .. 1:
let l = mk("loop" & $i)
discard l
# every `result` shape: each must construct in place, not `=sink` over zeroes
block:
let x = mk("direct")
let y = mkVia("via")
discard x
discard y
# last read is a move, a re-read is a copy
proc moves(): string =
var m = mk("moved")
result = consume(m)
proc copies(): string =
var k = mk("kept")
result = consume(k) & "/" & k.tag
discard moves()
discard copies()
echo log
#!STEP expect: @["d(proc)", "d(toplevel)", "d(loop0)", "d(loop1)", "d(via)", "d(direct)", "d(moved)", "c(kept)", "d(kept)", "d(kept)"]