mirror of
https://github.com/nim-lang/Nim.git
synced 2026-09-17 18:44:53 +00:00
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.
69 lines
1.9 KiB
Nim
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)"]
|