Compare commits

..

76 Commits

Author SHA1 Message Date
araq
39b2f1830f IC: break down processTopLevel, and refute the obvious frontend win
The frontend's loading is 3.55s of Atlas's 9.2s and had no breakdown
finer than "TopLevel 1459ms". Now it does — `tTopReplay`, `tTopLogOps`,
`tTopOffers`, `tTopStmts` and a counter for the records the loader
skips:

    TopLevel 1459ms = Offers 569 + ExportBranch 312 + LogOps 137
                      + the bare cursor walk ~371 + Replay/Stmts ~11

That immediately suggests a target, and it is a trap. 80% of every
module header the loader walks is tooling-only records — `sig`, one per
signature-symbol occurrence, plus `expansion`/`modulesrc` — which it
skips on sight and which exist only for `idetools`: 3.36M of 4.19M nodes
on Atlas. Grouping them under one tag, or moving them past the
`(implementation)` marker where the loop stops, is an easy change and
buys nothing.

Probed before building any of it, by emitting none of them at all:

    nodes walked  4.19M -> 0.85M      .s.bif    44.7MB -> 44.0MB
    TopLevel      1459ms -> 1406ms    frontend  9.29s  -> 9.18s

Walking 3.3M records costs 53ms. `skip` on a `TagLit` is a jump, not a
scan — about 16ns a node — so the count was never the cost. A format
change for 0.5% would have been a bad trade discovered late.

What is left is the per-process re-load itself: 180 `nim m` each parsing
~20 modules' interfaces out of 44.7MB of `.s.bif`, with no dominant item
because there is no single item. It is the amortisation problem batching
already solved for the backend (`loadDepClosure` 10.2s -> 1.3s), and the
frontend is where it has not been solved.

Behind `-d:icBNodeProf`. ic 42/42, `koch boot -d:release` equal
executables.
2026-08-31 20:30:36 +02:00
araq
c6f70374bc IC: make timed arm the profiler, and price the C compiler
`timed` recorded into `profNanos` but never called `armProf()`, so a
process whose only instrumentation is a `timed` never registered the exit
dump and reported nothing at all. The `merge`, `emit` and `link` stages
have been silently absent from every profile in this branch — including
the one I used to claim the backend was fully accounted for.

With them present, Atlas cold at batch size 16, parallel (9.66s elapsed):

    frontend   181 proc   10.60 s summed process wall
    lower       14 proc    4.49 s
    cg          14 proc    4.50 s
    merge        1 proc    0.20 s
    emit        14 proc    0.42 s
    link         1 proc    1.65 s   <- the entire C compile + link

That settles the C compiler as a target: its 12.2s of CPU is 1.65s of
wall, because `callCCompiler` fans out across cores, and the excess over
a whole-program build is ~0.4s of it. `bnode.nim` records where the
excess is — 3.8MB of the 5.4MB is per-TU prototypes and typedefs, which
is intrinsic to emitting 204 translation units instead of 139 — and why
the obvious sub-target is not one either: 53 of the 204 object files
define nothing, and compiling all 53 costs 0.23s of user time.

Fewer, larger TUs is the only real fix, and it trades directly against
what IC is for: a sandwich edit rebuilds exactly one `.c` and one `.o`.

Behind `-d:icBNodeProf`. ic 42/42.
2026-08-31 19:59:09 +02:00
araq
55b244efff IC: build a module's hidden interface on demand, not on every load
`interfHidden` was 1.05s of a cold Atlas build: 1.70M hidden-symbol
stubs against 0.29M exported ones, created by every `nim m` for every
module it imports. The table is reached ONLY through
`modulegraphs.interfSelect` with `optImportHidden`, and that flag is set
in exactly one place — an `import x {.all.}`. Almost nothing reads it.

So it is built when something asks. Every read goes through
`interfSelect`, so guarding its four call sites is complete;
`modulegraphs` already imports `ast2nif`, so the call is direct.

The reason the first attempt at this failed, recorded because it is not
guessable: **a module has two FileIndexes.** `registerNifSuffix` keys
`filenameToIndexTbl` by the NIF SUFFIX and mints a `fikNifModule` entry,
while the graph indexes `g.ifaces` by the module's `fikSource` file, and
`DecodeContext.mods` is keyed by the former. Asking it with the latter
misses every time, silently — `import x {.all.}` then reported
"undeclared identifier" for a symbol that was right there. The lazy
builder therefore takes a SUFFIX. Two more conditions are load-bearing:
clear the pending flag only when the build SUCCEEDS, since an import
whose `.s.bif` the build has not produced yet must be retried rather than
written off for the rest of the process; and build into a LOCAL table
before assigning it back, since loading symbols can grow `g.ifaces` and
leave a `var` alias into it dangling.

Atlas, 204 modules:

    frontend      9.98s -> 8.96s      loading  4.57s -> 3.55s
    InterfTables  1161ms -> 79ms      hidden stubs  1.70M -> 0
    cold serial  21.59s -> 19.79s     cold parallel  11.11s -> 10.47s

Also fixes a scanner gap the test exposed. `import x {.all.}` serialises
as `(pragmax x (pragmas all))`, which `deps.nim.parseImportPath` did not
recognise, so it fell into the unknown-subtree skip and the import was
DROPPED from the static graph — the build only learned about it from the
`.s.deps` sidecar a round later, after a round that failed with
"requires precompiled NIF for import". Correct, but a wasted round and an
alarming error line for an ordinary import.

`tests/ic/timporthidden.nim` covers it: `{.all.}` sees the private
symbols, and the sibling case (a plain `import`) still rejects them under
both `--ic:on` and `--ic:off`. ic 42/42, `koch boot -d:release` equal
executables.
2026-08-31 19:38:07 +02:00
araq
f48efa4b1f IC: record that the lazy interfHidden conversion does not work yet
Tried it. The shape is easy and it is not the problem: `modulegraphs`
already imports `ast2nif` so no callback hook is needed, every read of
`interfHidden` goes through `interfSelect`, and its four call sites all
have the graph and the module to hand.

It builds, and it keeps privacy — a plain `import` still rejects a
private symbol under both `--ic:on` and `--ic:off`. But `import x {.all.}`
then fails under `--ic:on` with "undeclared identifier", where it works
today and works under `--ic:off`. Silent, and entangled with the driver's
discovery loop: the module is not in `DecodeContext.mods` when the lookup
asks, on a cold cache and on a warm one alike.

Three fixes tried, none of them it, recorded in `bnode.nim` so nobody
re-guesses them: clearing `hiddenPending` only on success (necessary, not
sufficient — one early miss otherwise costs the module its hidden symbols
for the whole process); loading the module inside the lazy builder rather
than assuming the caller did; and building into a local `TStrTable` in
case the `var` alias into `g.ifaces` was being invalidated by the seq
growing underneath it. After all three, `ensureHiddenIface` still reports
the module missing on every one of its 19 calls.

So the next attempt starts by instrumenting `moduleId`'s FileIndex
against `moduleFromNifFile`'s — whatever sets `hiddenPending` and
whatever fills `c.mods` are not agreeing about which module they mean —
before any more of the mechanism gets written. The 1.05s is still there
and still worth having.

Comment-only. ic 41/41.
2026-08-31 19:00:48 +02:00
araq
3f014bbd37 IC: instrument nim m, and find 1.05s of interface stubs nobody reads
The frontend was the one phase with no timers inside it — 180 processes
and 9.9s of Atlas's cold build, entirely opaque. `tStage` around
`commandCheck` and `tWriteNif` around `writeNifModule` close that, and
with the loading slots ast2nif already had, a `nim m` process is now
fully accounted:

    startup (exec+runtime+config)    0.17 s    2%
    loading imported `.s.bif`        4.57 s   46%
    writing this module's `.s.bif`   1.84 s   18%
    sem + parse                      3.41 s   34%

Two thirds of the frontend is artifact I/O, not compilation. That is the
per-module model's structural cost: 180 processes each rebuild their
imports' interfaces from nothing.

One part of it is not structural at all. `interfHidden` accounts for
1.05s of the loading: 1.70M hidden-symbol stubs against 0.29M exported
ones, built by every `nim m` for every module it imports. The table is
reached ONLY through `modulegraphs.interfSelect` with `optImportHidden`,
and that flag is set in exactly one place — an `import x {.all.}`. The
backend already skips building it for this reason; the frontend cannot
skip it unconditionally, but it does not have to build it eagerly either.

Measured with a probe rather than estimated: skipping it takes
`InterfTables` 1161ms -> 80ms, the frontend 9.98s -> 8.93s, and the whole
Atlas build 22.19s -> 20.47s wall.

Not taking it in this commit. The correct form is lazy population on
first `interfSelect(true)` — deciding up front cannot work, because a
macro-generated `{.all.}` import is invisible syntactically and guessing
wrong loses symbols silently. `loaderCtx` is the hook (the module index
survives in the DecodeContext), but this is symbol visibility, and it
deserves its own pass rather than the tail of a long one.

`bnode.nim` carries the numbers and the design note. All behind
`-d:icBNodeProf`. ic 41/41, `koch boot -d:release` equal executables.
2026-08-31 17:03:05 +02:00
araq
d680599038 IC: instrument the backend phases, and tag which process each line is
I claimed 19s of the Atlas backend's CPU was work no timer accounted
for. It is not, and the 19s was an arithmetic artifact: I subtracted a
serially-measured frontend and a separately-measured gcc from a
PARALLEL build's CPU total. Mixing the two is meaningless.

Measured properly, with new coarse slots — `Stage` for the whole stage
body (so `Process - Stage` is exec + runtime init + config replay +
graph setup) and per-stage `LowerOwned`/`LowerHooks`/`LowerWrite`,
`CgGen`/`CgInit`/`CgFinish`/`CgWrite` — Atlas at batch size 16,
serially:

    stage      procs  process wall   in stage   startup
    frontend     180        9.90 s     0.00 s     9.90 s
    lower         14        3.55 s     3.50 s     0.04 s
    cg            14        3.27 s     3.23 s     0.05 s

      lower: LoadClosure 756ms  Owned 917ms  Hooks 4ms  Write 1681ms
      cg:    LoadClosure 548ms  Gen 1912ms  Init 168ms  Finish 169ms  Write 464ms

So the backend's nim-side work is 6.8s, not 19s, and 6.7s of it is
inside the stage bodies with 0.09s of startup across 28 processes —
batching having already removed the per-process cost that used to
dominate. The two largest items are writing the `.t.bif` (1.68s) and
cg's demand-driven generation (1.91s, of which `genProcBody` is 0.6s).
The bulk of the build is elsewhere: the frontend's 9.9s and gcc's 12.2s.

The dump line now carries `stage=<name>`, and that is not cosmetic. A
`nim m` process arms the profiler through ast2nif but never enters a
backend stage, so untagged, those 180 frontend runs put their entire
runtime in the "startup" column — which is exactly the phantom that made
the 19s look plausible in the first place.

All of it is behind `-d:icBNodeProf` and compiles to nothing without it:
ic 41/41, `koch boot -d:release` equal executables.
2026-08-31 16:19:43 +02:00
araq
087c82f985 IC: two assumptions that only held while a cg process wrote one TU
Both are broken by batching, and both produce C that does not compile.
Found by pointing a batched `--ic:on` at Atlas; neither shows up on a
small synthetic target.

**A local's C name is cached on the PSym, its counter lives on the
BProc.** `fillLocalName` mints `i_1` and stores it on the symbol, taking
the suffix from `p.sigConflicts`. Emit the same routine into a second TU
and the new BProc's table has never seen `i`, while the body's locals
already carry names and skip the minting path entirely — so the next
local named `i` minted HERE also gets `i_1`. gcc: "redeclaration of
'i_1' with no linkage", in 64 of Atlas's 204 `.c` at batch size 4. So a
local that arrives already named claims its name in this BProc.

Note this is not purely a batching concern: `assignLocalVar` already
says inline procs "are regenerated for each module that uses them", which
is the same shape.

**`genProcPrototype` asserted its own routing.** Its IC branch emits
nothing for a dynlib proc, on the stated grounds that "findPendingModule
returns `m`, so symInDynamicLib follows this call" — true when a process
wrote one TU, false once a batch sibling can own the definition. The
demander then had neither the definition nor an extern for the `Dl_*`
pointer: "implicit declaration of function 'Dl_369101372_'" in net.nim's
OpenSSL calls. It now asks whether a sibling owns it, mirroring
`findPendingModule` rather than calling it — that proc creates a BModule
on demand, which a prototype has no business doing.

Atlas, 204 modules, cold, with both fixed — every batch size builds and
produces a working binary:

    batch    wall      user      sys    CPU
      1     11.51 s   52.29 s  16.39 s  68.7 s
      4     10.34 s   41.53 s  10.93 s  52.5 s
     16     10.21 s   36.01 s   8.48 s  44.5 s
     32     10.52 s   33.66 s   8.01 s  41.7 s

CPU -39%, wall -11% and flat past 16 — the backend's 41 s of CPU for
4.8 s of wall was largely process startup and re-loading the same
dependency closure, and batching is what stops paying it. Duplication is
NOT what moved: 1.95x -> 1.90x, because `findPendingModule` routes to the
owner only when the owner is IN the batch and most demands are for
modules outside it. That 2x is a separate seam.

The default is still batch size 1, where both fixes are inert by
construction: 204 of 204 `.c` byte-identical to the previous commit.
ic 41/41, `koch boot -d:release` equal executables.
2026-08-31 15:38:15 +02:00
araq
bff0bc45fd IC: export s makes s importable even without a * on its declaration
`nim c --ic:on` could not compile anything that reached `std/tempfiles`:

    lib/std/tempfiles.nim(115, 38) Error: type mismatch
    Expression: initRand()
    Expected one of: [1] proc initRand(seed: int64)

That is `std/random`. It declares `proc initRand(): Rand` WITHOUT a `*`
and then, forty lines later, `since (1, 5, 1): export initRand`.

Two mechanisms make a symbol importable and the writer only knew one.
`sfExported` is the `*` on the declaration; `semExport` instead calls
`reexportSym`, which adds the symbol to the module's interface table and
sets no flag. `writeSymDef` decided the NIF `x` marker — "importable as a
bare identifier" — from `sfExported` alone, so a symbol exported the
second way shipped as private and its importer reported an undeclared
identifier. Nothing about this is IC-specific in the source; it is
ordinary stdlib code that classic compilation accepts.

So ask the interface too: `reexportedLocalSyms` collects the symbols a
module defines that reached its interface without the flag, and they get
the marker. Symbols that are genuinely private still do not — the
regression test's sibling case (`proc g()`, never exported) is still
rejected under both `--ic:on` and `--ic:off`.

This was found by pointing `--ic:on` at Atlas, which is now the first
sizeable third-party program it builds. `nim c --ic:off` and `--ic:on`
produce the same working binary from the same 209-module closure.

ic 41/41 (the new test is the 41st), `koch boot -d:release` equal
executables.
2026-08-31 14:51:25 +02:00
araq
8299401888 IC: render every module's .c in one emit process
`emit` is the one backend stage with nothing to get wrong. It loads no
module graph — it derives its output path rather than resolving it — and
`renderCFromArtifact` filters text against the merge decision without
touching an AST. It is a pure function of one `.c.nif` and one global
artifact, so grouping the calls cannot change what comes out.

Measured rather than assumed: 67 separate emit processes and one process
with all 67 members produce byte-identical `.c`, in 0.502 s against
0.041 s. So emit takes a single nifmake rule covering every live module.

Where that shows up is narrower than the 12x suggests. Cold serial 8.21 s
-> 7.82 s; cold parallel 2.81 s -> 2.83 s, i.e. nothing, because 0.46 s
spread over 16 cores is already invisible. An edit to `system.nim` is
2.15 s either way: emit was never what made that slow. The honest case is
CPU rather than wall clock, plus one fewer thing to pay for on a machine
that is not idle — the fire-all is structural (every `emit` re-fires
whenever `merge` rewrites the decision, by design) and now costs one
process start instead of 67.

Also fixes a bug this exposed: `isMain` was a per-BATCH flag. That is the
right question for `lower`/`cg`, where main loads the whole program and
is never batched with anything, but emit batches freely — and main in a
mixed batch had its `.c` path derived from its module SUFFIX instead of
its source file, so it was never written at all.

`-d:icBatchSize:N` still splits emit the old way, for comparing against
the fan-out. ic 40/40, `koch boot -d:release` equal executables, and the
67 generated `.c` are byte-identical to the previous commit's.
2026-08-31 13:59:46 +02:00
araq
5d77a71043 IC: stop baking a compile command the process cannot know
Every generated `.c` carries a "Command for C compiler" comment built
from `conf.compileOptions`. Under the per-module backend that is a lie,
and a non-deterministic one.

A global `{.passC.}` — system's `-pthread`, say — reaches
`conf.compileOptions` only in a process that compiled the module
declaring it, and a `cg` process sees one module's import closure. So
the command each `.c` records is a partial snapshot, and WHICH part
depends on how modules were grouped into processes. Measured on a
67-module program: 2 of 67 `.c` carried `-pthread` at batch size 1, 4 at
size 4, 5 at size 8 — against 16 of 16 for a whole-program `nim c`.

The object files were never affected: the `link` stage applies every
module's recorded directives (`replayer.applyBackendActions`) before
compiling anything. Only the comment was wrong. But it is also the reason
two `.c` files differed between batch sizes for no reason of their own,
which makes it noise in the one oracle that matters here — the final `.c`
set.

So under `--icBackendStage` the line says where the command actually
comes from instead of guessing at it. Whole-program `nim c` is untouched:
byte-identical `.c`, and it still prints the real command, which there it
genuinely knows.

ic 40/40, `koch boot -d:release` equal executables.
2026-08-31 11:18:45 +02:00
araq
e5bba5a0fe IC: let one backend process handle a batch of modules
The plumbing behind the routing fix. `--icBackendModules:<a,b,c>` gives
the lower/cg/emit stages a LIST, `loadDepClosure` loads the batch's union
closure once instead of once per module, each stage loops over its
members, and `deps.nim` emits one nifmake rule per batch declaring all
its members' outputs. `-d:icBatchSize:N` is the dial; `0` means one batch
per job.

The default is 1, which reproduces the per-module fan-out exactly: `.c`,
`.c.nif` AND `.t.bif` byte-identical to the previous commit, ic 40/40,
`koch boot -d:release` equal executables.

Turning the dial up found three real bugs, none of which could exist
while a process wrote one TU:

* `loadDepClosure` deduplicated batch members against `visited`, which
  already contains system — so a batch whose member IS system loaded
  nothing and produced no artifact at all. System is an ordinary live
  node with its own `.t.bif`/`.c.nif`; members now have their own set.

* `cg` finished each member's TU as it went. `finishModule` closes a TU,
  and a later member's codegen routes definitions INTO an earlier
  member's TU, which then silently dropped them. Generate every member,
  then finish every member.

* `emitsBodyInThisModule(m, prc)` was asked with the DEMANDING module
  where it means the module the body goes INTO. Identical while
  `findPendingModule` always returned `m`; with a batch the definition
  was marked declared in its owner's TU and then emitted by nobody — 18
  undefined symbols at link.

Where it stands at batch size 4 and 8: builds, links, runs correctly, and
CPU drops from 9.8 s to 7.2 s / 6.2 s on a 67-module program. But the
artifacts are NOT invariant along the dial — 16 of 67 `.c` differ at 4 —
and the divergence is of two kinds. Most are the recorded gcc command
comment: per-module `{.passC.}` flags leak between batch members through
`writeBackendActions`. The rest is real: the set of minted type-bound
hooks moves (four `=destroy`/`=trace`/RTTI hooks vanish, one appears),
because which process mints a hook decides who owns it and batching
changes that. Duplication is also still 2.19x, unchanged — at these
sizes most demands are still for modules outside the batch.

So the dial stays at 1 until that invariant holds. It exists to be
turned, and it now reports what it finds when you do.
2026-08-31 10:53:14 +02:00
araq
bad9d3b2bc IC: route a cg definition by who owns it, not by who asked
`findPendingModule` decides which translation unit a demanded definition
is emitted into. Under `--icBackendStage:cg` it was `return m` with a
`# TODO fixme` over it: the destination was whichever TU did the asking,
because the stage was built around a single `--icBackendModule` and
there was never a second answer to give.

That hardcoding is what makes the backend inflexible to batching. It is
not that codegen cannot place a definition with its owner — the branch
four lines down already does exactly that, creating the `BModule` on
demand — it is that the cg stage could not express "this process writes
several modules' TUs", so the question never got asked.

So ask it. `BModuleList.icEmitted` is the set of module positions this
process writes a TU for, and routing consults it: an owner in the set
gets its own definition (the ordinary whole-program routing, now
reachable from cg), an owner outside it means the definition has nowhere
else to go and is emitted here as well — today's emit-everywhere, which
`merge` still deduplicates. Duplication therefore falls smoothly as the
set grows rather than switching over at some threshold.

The stage puts one module in the set, so this run is a no-op by
construction: the owner is `m` and the branch returns what `return m`
returned. Verified as one — a 67-module `--ic:on` build produces `.c`
AND `.c.nif` byte-identical to the previous commit's, `testament cat ic`
is 40/40, and `koch boot -d:release` reaches "executables are equal".

A change that provably alters nothing needs a live-probe check, or it is
indistinguishable from dead code. Temporarily adding `system`'s position
to the set — a module these processes do NOT write — moved 12 generic
instantiations (`addQuoted_i*`, `clamp_i*`) out of the TUs that demanded
them and into a TU nobody wrote, failing the link with exactly those 12
undefined symbols and shrinking generated `.c.nif` volume by 4%. The
routing is live; what it still lacks is the plumbing to write a TU per
batch member, which is the next step.
2026-08-31 10:18:09 +02:00
araq
98b1e0ab50 IC: own nifstreams here, and move the nimony pin to 1721aab3
`nifstreams` is the classic NIF streaming surface — global `pool`,
`PackedToken`, `next(s)` — that this compiler's IC modules (ast2nif,
deps, modulegraphs, pipelines) are written against. It lived in
`dist/nimony/src/lib` even though nimony's own code is under standing
orders never to import it, so nothing over there ever ran a line of it.

That is how it broke. Nimony's nifcore refactoring unified the token
kinds, and the adapter's `next` started returning `tagLitToken(...)` for
an opener — kind `TagLit`, not the `ParLe` its own header promises
structural scanners. Nothing in nimony noticed. Here, `deps.nim` walks
the import graph by testing exactly `t.kind == ParLe`, and the failure
has no error path: an opener it does not recognise is just an unknown
token, so it skips the subtree and reads on. Every `.deps.nif` still
"parsed"; the graph came out missing most of its edges. `nim c --ic:on`
then scheduled modules ahead of their imports and died on a `.s.bif`
nothing had written yet:

    Error: nim m requires precompiled NIF for import: .../streams.nim
           (expected: .../str6a47nh.s.bif)

`json`'s rule listed 5 of its 10 dependencies.

So the adapter moves into `compiler/`, where its one consumer lives, its
test runs, and its contract is ours to keep. `next` now emits a real
`ParLe`, with the tag id in the full 28-bit payload rather than
`TagLit`'s 9-bit field and a `tagId` shadowing nifpools' to match:
`globalTags` holds 355 tags before this compiler registers one of its
own dialect, and a 512-tag ceiling is not one this surface can live
under. The `when isMainModule` self-test states the promise; reinstating
`tagLitToken` fails it on the first assertion.

Everything the adapter adapts (nifpools, nifreader, lineinfos) still
comes from `dist/nimony` — only the adapter moved. With it here, the pin
can move to nimony master: `bif.load` there fills a loaded module's
pools with `addOrdered` instead of hashing every entry it just read back
in stored-id order.

Verified: `koch boot -d:release` reaches "executables are equal",
`testament cat ic` is 40/40, and a 67-module `--ic:on` build produces
`.c` files byte-identical to the old pin's. Interleaved on that build,
old pin 8.40/8.39/8.43 s vs new 8.17/8.14/8.14 s.
2026-08-31 09:30:12 +02:00
araq
82b1b9a3c9 koch: rebuild nifler/nifmake when the nimony pin moves
`bundleChecksums` built the two host tools only `if not fileExists`, so
bumping `NimonyStableCommit` in a tree that already has `bin/nifler`
leaves them on the previous commit while the compiler links the new
`dist/nimony/src/lib`. The two halves of the IC toolchain then disagree
about the on-disk formats, with nothing saying so.

The failure mode is the wrong way round: a fresh CI checkout has no
`bin/`, builds both tools from the new pin and goes green; only a
working tree that already has them breaks. Stamp each tool with the
nimony commit it was built from and rebuild when that no longer matches.
Where the commit cannot be read (a bundled `dist` with no `.git`), keep
the old build-if-absent rule rather than rebuilding on every boot.
2026-08-31 09:14:50 +02:00
araq
b6248a0b80 IC: read an exported symbol's kind from its header, not by decoding it
`addReexportedEnumFields` forced every exported symbol through `loadSym` and
THEN asked whether it was a non-pure enum type. Almost none are: 34815 symbols
on a 68-module build, of which the enum handling wants the handful that are
types. A sym def is written `(sd <name> <marker> <kind> …)`, so the kind is
three tokens in and needs no decode at all.

    addReexportedEnumFields   290ms ->   5ms
    export branch             484ms -> 154ms
    processTopLevel          1086ms -> 787ms
    loadDepClosure           2126ms -> 1978ms
    cold --ic:on build         8.69s -> 8.51s   (baseline built alongside)

`peekSymKind` mirrors `loadSymFromCursor`'s walk and the two have to change
together, so it is graded rather than trusted. `-d:icPeekKindCheck` compares
every peek against the load it replaces: a full build is 34508 peeks, zero
disagreements, and sabotaging the peek to answer `skProc` where the def says
`skType` fires on the first symbol.

The FIRST sabotage did not fire, and that is the part worth recording. Dropping
a `skip` from the walk lands on a non-`TagLit`, which answers `skUnknown` — the
designed fallback, correct but slower — so the equality assertion never saw it.
A walk that had drifted out of step would therefore look exactly like a clean
run. So the check has a second half: `PeekFallback` counts how often the peek
cannot read the header and `-d:icBNodeProf` reports it beside `PeekKind`. It is
0, which is the claim that the walk is in step; an equality oracle alone could
not make it.

Verified: both configurations build; `tests/ic` 40/40; 67/67 generated `.c`
byte-identical, cursor still identical to `PNode`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-31 00:44:23 +02:00
araq
4c6d7d2d20 IC: time the export branch's two halves, so the next lever is sized
`processTopLevel`'s `export` branch is 484ms of an 8.64s build, and the split
matters for what to do about it: `resolveSym` is 118ms, `addReexportedEnumFields`
is 290ms over 34815 exported symbols — 8.3us each.

That cost is one line: `addReexportedEnumFields` calls `loadSym` to force the
symbol out of `Partial` state and THEN asks whether it is a non-pure enum type.
Almost none of them are, so nearly all of that is a full symbol decode done to
answer a question the def's header already contains — a sym is written as
`(sd <name> <marker> <kind> ...)`, so the kind is two tokens in.

Not fixed here, deliberately. A peek would have to hand-walk that layout, and
mis-stepping it misclassifies silently rather than failing; it is worth doing
with an oracle beside it, the way `indexFromBif` was. Sized and recorded so the
next person starts from a number instead of a hunch.

Timers only — no behaviour change; verified 67/67 `.c` byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-30 23:55:58 +02:00
araq
1a70426a5b IC: dispatch the top-level walk on a tag id, not on ~20 tag names
`processTopLevel` decided what a top-level node was with an `elif` chain of
about twenty `tagIs` calls, each of which resolves a tag NAME and compares
strings. The two commonest outcomes — a real statement, and `implementation` —
sit at the END of that chain, so the average node walked all of it. There are
1.46M such nodes on a 68-module build.

Resolved once per tag id into a `TopTag` and dispatched with a `case`. The memo
holds its `TagPool` by reference for the same reason `indexFromBif`'s and
`bnode`'s do: that is what keeps a freed pool from being replaced at the same
address and answering from the wrong table.

    processTopLevel   1314ms -> 1071ms
    loadDepClosure    2309ms -> 2126ms
    cold --ic:on build 8.92s -> 8.64s

The last `elif` folded three conditions together (`LoadFullAst in flags` OR one
of let/var/pragma); as a `case` that splits into the three tags, which always
load, and `ttOther`, which loads only under `LoadFullAst`. Same truth table,
now visible.

A note on the measurement, because it nearly cost this change: the first timing
of it read 11.35s against a remembered 8.91s. Re-running BOTH binaries
back-to-back gave 8.914/8.931 for the old and 8.641/8.641 for the new — the
first number was a `koch temp` build still settling. Compare against a baseline
measured beside it, never against one from earlier.

Verified: both configurations build; `tests/ic` 40/40; 67/67 generated `.c`
byte-identical, cursor still identical to `PNode`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-30 23:52:08 +02:00
araq
20a3375571 IC: a backend stage stops building the interface half it cannot read
`populateInterfaceTablesFromIndex` stubbed a `PSym` for every global symbol a
module declares and inserted it into `interf` and/or `interfHidden`. On a
68-module target that is 2.37M stubs across the build — and 2.04M of them go
into `interfHidden` alone, which a `cg`/`emit` stage has no way to reach.

`interfHidden` is selected exclusively by `modulegraphs.interfSelect`, and only
when `optImportHidden` is in the module's options. That flag is set in exactly
one place — `importer.importModuleAs`, i.e. during sem. A backend stage builds
its module symbols in `moduleFromNifFile` and never imports anything, so the
flag cannot be set and the table cannot be selected. Skipping the hidden-only
branch there is unobservable, not merely unlikely to be observed.

Exported symbols still go into both tables. That is 0.33M inserts against the
2.0M saved, and it leaves `interfHidden` a coherent view — a module with no
hidden symbols — rather than an empty one, should anything ever consult it.

    interface tables   1401ms -> 863ms
    loadDepClosure     2771ms -> 2309ms
    cold --ic:on build  9.38s -> 8.91s

Skipping the tables WHOLESALE in a backend stage does not work, and the failure
is worth recording since it is the obvious next thought: the exported half is
what `magicsys.getSysSym` resolves through, so the stage dies with "system
module needs: pointer". Skipping them in the FRONTEND is worse still — `nim m`
loses `defined`.

Verified: both configurations build; `tests/ic` 40/40; 67/67 generated `.c`
byte-identical, cursor still identical to `PNode`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-30 23:42:06 +02:00
araq
2605cd3213 IC: take the module index from the .bif instead of rescanning for it
`moduleId` threw away the index `bif.load` hands it and rebuilt an equivalent
one by walking the module's ENTIRE token stream, allocating a string per
`SymbolDef` — once per module, per backend process. That was 909ms of a 10.1s
cold `--ic:on` build. Taking the carried index instead: 220ms, and the build
drops to 9.4s.

`bif.store` already builds that index in one forward traversal at write time
(`bif.buildIndex`) and writes it into the file, and `bif.load` reads it back
with `pos` already a TOKEN index of the declaration's enclosing tag — the very
thing the rescan recomputed. The comment claiming the file's offsets are
"meaningless once the file is parsed" was true of the older byte-offset
`readEmbeddedIndex`; it stopped being true when `bif` started storing token
positions.

The two agree BY CONSTRUCTION, and the reason is worth stating because "the
file has an index" would not be enough on its own: the writer filters with
`bif.isGlobalSymbol(name, dottedSuffix)`, every `storeBif` call site passes
`"." & extractModuleSuffix(path)` — the same suffix the reader forms — and the
visibility rule is the same test on the same token.

Checked rather than argued, all the same. The old rescan stays as
`rescanPosIndex` behind `-d:icIndexCheck`, which compares the two entry by
entry on every module load: a full build agrees exactly (11220 entries for the
system module alone), and sabotaging the comparison makes it fire, so the clean
run says something.

`ensureSemBuf` had the same rescan for the `.s.bif` companion; it uses the
carried index too.

Verified: both configurations build; `tests/ic` 40/40; 67/67 generated `.c`
byte-identical to before, and cursor still identical to `PNode`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-30 23:30:30 +02:00
araq
dbaed3d38a IC: measure the whole backend, and record where its time actually is
The profiling added with the cursor path was local to `bnode`, so it could only
answer questions about the cursor path. Moved to `compiler/icprof.nim` — no
compiler imports, so any stage can use it without a cycle — and extended to the
stage boundaries: the closure load and its three phases, `transformBody`,
`handOffBody`, `genProcBody`.

The budget that produces, on a cold `--ic:on` build of a 68-module target
(10.1s wall, summed over 177 backend processes):

    loadDepClosure   3306ms     of which  moduleId        1285ms
                                          processTopLevel 1516ms
                                          interface tbls  1323ms
    genProcBody       333ms
    handOffBody        60ms
    transformBody      28ms

This is worth having written down because it reprices the migration this branch
is doing. Reading a routine body off a cursor rather than a tree is finished and
costs nothing — `genProcBody` is the same either way. But FINISHING the job,
reading a `.t.bif` body directly and never materialising the `PNode`, can only
win back `handOffBody` + `transformBody`: under 1% of the build. The 41% is in
getting the closure's INTERFACE into memory, which no amount of body-reading
touches.

The remaining blockers in `bnode`'s header — `TLoc.lode` above all, 72 call
sites and hard, because a symbol's `loc.lode` outlives the body it was built in
and `lode == nil` is a sentinel — are worth exactly that under 1% until
something else changes. Said so in the header, replacing the older 0.20s/0.16s
figures, since that paragraph is the map read first.

The obvious lever on the real cost was tried and is not taken:
`{SkipInterfaceTables}` for dep-of-a-dep loads in `loadDepClosure` builds and
runs correctly but returns ~200ms, because most of that phase is the target and
system modules rather than the transitive ones. Not worth a name that silently
fails to resolve, so the flag stays restricted to `loadTransitiveHooks`.

Verified: both configurations build; `tests/ic` 40/40; the instrumentation
changes no codegen — 67/67 `.c` identical, cursor still identical to `PNode`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-30 23:09:32 +02:00
araq
727d70d2ba IC: keep the node bridge out of the nimKochBootstrap build
`koch boot` failed with `bodynav.nim(278) Error: undeclared identifier:
'program'`. `transf` imported `nodebridge` unconditionally, which pulls in
`bodynav`, which resolves names through `ast.program` — and `program` does not
EXIST under `-d:nimKochBootstrap`. That define disables the IC subsystem
wholesale: `ast.nim` guards `program` and the loader callbacks with it, and
`koch.bootic` records that it also disables `commandIc`. The bridge was being
compiled into a build with nothing for it to talk to.

Guarded the same way `ast.nim` already guards `program`. `handOffBody` goes
with it; its only caller is `cgen`, under `-d:newIcBackend`.

`koch temp` cannot catch this — it does not set the define — so the break
survived several rounds of verification that all used it. Predates this
branch's recent work: `transf`'s unconditional import and `bodynav`'s use of
`program` are both there at 8afd306b0.

Also moved `bnode`'s `std / assertions` import inside the `newIcBackend`
branch, the only one that asserts. At module scope it warned "imported and not
used" on every default build, which is how it showed up in the boot output.

Verified: `koch boot -d:release` reaches "executables are equal: SUCCESS!";
both non-bootstrap configurations build; `tests/ic` 40/40; the generated C is
unchanged — 67/67 identical to before this commit, and cursor still identical
to `PNode`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-30 21:10:49 +02:00
araq
2db27d4826 Merge branch 'devel' into araq-ic-fixes2
One conflict, in `canRaiseDisp`, where both sides added to the same guard:

* devel (#26145) short-circuits `skMethod` to "can raise", because a base
  method's inferred effects describe only the base body, not every vtable
  target;
* this branch resets `markCanRaiseBranch` to 5 on entry, so that a `-d:
  icCanRaiseLog` differential attributes an answer to the branch that actually
  decided it rather than to whatever the previous call left behind.

Both kept: the reset first, then devel's method branch, then the existing
flags branch. Marker 5 already means "decided here, neither predicate ran",
which is what the new branch does too, so it needs no new number — the comment
now says both short-circuits land there.

Verified on the merged tree: `tests/ic` 40/40, including devel's new
`timportcalias`; the cursor-driven and `PNode`-driven backends still generate
byte-identical C (67/67 under `--ic:on`); ccgbugs 146, concepts 48, method 22,
destructor 97, arc 140, gc 78, closure 23, iter 71, exception 47, all clean.

`tests/generics/tparser_generator.nim` fails, and does NOT come from this
merge: it fails the same way on pristine `origin/devel`, built and run in a
throwaway worktree to check. The compile succeeds; the spec expects no output
and the compiler now emits `typed`-deprecation and unused-import warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-30 20:38:00 +02:00
araq
d27e1e2712 IC: give the cursor path a way to be measured, and record what it cost
The last commit's finding took three refuted hypotheses to reach. The switches
that produced it are kept, and so is the answer, because the next person will
ask the same question:

  * `-d:icBNodeProf` counts every accessor and times the phases (`handOffBody`,
    `genProcBody`, the analyses, `sym`/`typ`/`info`/`origin`). Each backend
    process appends a line to `$NIM_IC_BNODE_PROF`, so a parallel build still
    produces attributable output. Costs nothing when the define is off.
  * `-d:icBridgeOnly` builds the buffer but generates off the tree. It is a
    MEASUREMENT switch, not a mode, and it is the only way to separate what the
    encoder costs from what reading costs — which is how encoding was shown to
    be free and the reader identified as the thing to profile.

Cursor-driven generation is now the same speed as tree-driven: over the 2420
routines of a 68-module target, `genProcBody` is 369ms off a cursor against
366ms off a `PNode`. It was 1877ms.

The three suspects that measured out as wrong are written down so they are not
re-guessed: the tag-name string compares in `typ`/`flags` (0.3% of the build),
`Cursor`'s reference-counting lifetime hooks (6M calls, 60ms of 1900ms), and
the structural accessors as a whole — `kind`, `son` and the iterators together
are 32ms of 1877ms.

The one real change here is the tag memo, which now resolves a tag's
`TNodeKind` and its wrapper role together instead of comparing tag NAMES on
every `typ`/`flags`/`hasExplicitNilType` call. Small — it is the 0.3% above —
but it removes 197k string comparisons and lets those three read a `case`.
`tagCachePool` holds the pool by reference for the same reason `readPool` does:
that is what stops a freed pool from being replaced at the same address.

Verified with the parent commit: `tests/ic` 39/39, grinder clean, `.c`
byte-identical both under `--ic:on` and without it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-30 20:13:06 +02:00
araq
db47363784 IC: memoize the FileId -> FileIndex resolution, halving a cold --ic:on build
`oldLineInfo` resolved every decoded token's line info by copying a path out of
the buffer's filename pool and hashing it through `msgs.fileInfoIdx`. Every
time — no cache, on a step that runs once per node the decoder loads and once
per statement the generator emits.

On a 68-module target that is 259k calls at 5.2us. Memoized per pool it is
34ms, and the cold build goes from 21.5s to 10.1s.

`revTab` could not serve this: it is keyed by a `FileId` in the WRITER's global
`pool.files`, while a decoded token's `FileId` indexes the buffer's OWN
filename pool — a fresh one per `bif`-loaded module. So the cache is keyed by
(pool, FileId), as a `seq` because ids are small and dense within one pool.

`readPool` holds a REFERENCE rather than a raw pointer, and that is the whole
safety argument: it keeps the pool alive, so a freed pool cannot be replaced by
a new one at the same address and answer from the wrong file table.

Verified: `tests/ic` 39/39; the cursor-vs-`PNode` differential grinder clean
over ~150k graded nodes on `tools/icgrind` — and confirmed live by sabotaging
the pool-identity check, which makes it fire on `info` immediately; all 67
`.c` files an `--ic:on` build generates byte-identical to before; all 216 a
non-IC build generates likewise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-30 20:11:36 +02:00
Araq
8afd306b0d IC: correct the module docs to the state the seam is actually in, and measure it
`bnode`'s header still said the generator "has to move together, and it needs
write-side capability this seam does not have". It has moved, and `origin` was
the write-side answer, so the section described a state two commits out of date —
the kind of stale map that sends the next person looking for a problem that is
already solved. Rewritten to say what runs on a cursor now, that `origin` is what
keeps `TLoc.lode` a `PNode`, and that the generator's own in-place rewrites run
on the origin (with the hazard spelled out: where a mutation is read back,
generation has to continue on the origin, because the buffer is a snapshot).

Two blockers were also described wrongly:

* THE ALIAS FAMILY is no longer blocked by field identity — that is exact on a
  bridged buffer. I checked whether `isPartOf` could migrate now and it cannot,
  for a different reason: every call site passes `d.lode` as one operand and that
  is a `PNode`, so a generic `isPartOf` would still be handed a `PNode` on one
  side and buy nothing. It moves when `TLoc.lode` does.
* `sym`'s note said `isPartOf` "CANNOT be migrated as written" without
  qualifying that this is a FILE-path property; a bridged buffer hands back the
  object it was given, and the grinder asserts exactly that.

And the cost, which I flagged twice as unmeasured and is now measured on a
50-module target:

    baseline (PNode)            6.75s
    bridge built, not read      6.79s   -- encoding is inside the noise
    generator driven off it     8.85s   -- +31%

So the encoder is as cheap as claimed and the whole cost is in READING: `son` is
O(i), `kind` indexes a memo per call, `sym`/`typ` go through the nav, `origin` is
a hash lookup per location built. None of it is inherent and none of it has been
optimised. Since a compile is mostly frontend, codegen itself is slowed by well
over 31%.

Verified: both configurations build; cursor-driven and `PNode`-driven output
still identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 16:25:13 +02:00
Araq
603953376d IC: add -d:icSymCount, and use it to settle the full-compiler comparison
Counts every `newSym` mint, by kind, and reports on exit. A gensym's number is
its item id, so ONE extra symbol anywhere shifts every later name in the
generated C — which makes a mint count far more sensitive than diffing output,
and localises a difference by symbol kind instead of by whichever file happened
to show it.

It was written to answer a question I had reported wrongly. Comparing the
cursor-driven and `PNode`-driven builds over the whole compiler showed 3 of 215
`.c` files differing, and I attributed 2 of them to the compiler being
nondeterministic. That control was invalid: I had compared two runs across an
edit to `ccgexprs.nim`, so what I read as nondeterminism was line numbers moving
inside assertion strings. The compiler is DETERMINISTIC — same binary, same
source, twice, 0 of 215 differ.

What the count establishes: the two builds mint exactly the same symbols,
515_551 of them, with no per-kind difference at all. So nothing in the cursor
path creates or skips a symbol, and the earlier gensym-numbered differences —
which had zero non-gensym lines — were ordering, not extra work.

On the current tree the comparison is clean: cursor-driven and `PNode`-driven
produce BYTE-IDENTICAL `.c` for all 215 files of `compiler/nim.nim`. I have not
isolated why the same comparison showed 3 differences one commit earlier; the
commit between was behaviour-neutral by inspection, so I am recording the
observation rather than a claim about its cause.

Verified: both build configurations compile; the diagnostic is behind a define
and costs nothing otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 16:16:08 +02:00
Araq
31a7b6bc85 IC: undo gratuitous rewrites the bulk migration made
Reading the previous commit's diff, as promised, turned up edits that were
semantically neutral but said something false: a regex pass had rewritten
`t.n == nil` (a TYPE's record tree) as `t.n.isNilNode`, done the same inside the
grinder where both sides are `PNode`s, and relabelled `asgnComplexity` and
`containsOpaqueImportcFieldAux` as `AnyNode` when both walk `PType.n` and can
never see a cursor. It also edited a comment.

None of it changed behaviour — `isNilNode` on a `PNode` is `== nil` — but it
made unrelated code look like it participates in the seam, which is exactly the
kind of noise that makes a later reader think the type-record walkers are
migration candidates when the module doc says they are not. Reverted, and the
two type-record procs now say why they stay.

Verified: both builds compile; cursor-driven and `PNode`-driven output still
identical (50/50).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 16:07:20 +02:00
Araq
709fd00861 IC: migrate expr and the emitters — cgen generates from a cursor
`genProcBody` is handed `BNode(bodyBuf.rootCursor)` under `-d:newIcBackend`, so
`expr` and the ~160 procs under it read the routine body through a cursor rather
than a tree. This had to land as one change: `expr` dispatches to all of them, so
they move together or the dispatch converts at every node.

The evidence that it works is not that it compiles. Cursor-driven and
`PNode`-driven builds emit BYTE-IDENTICAL `.c` (50/50 on an 89k-line target,
12/12 on the grind target), the built program runs and prints the right thing,
and — the part that makes the first number mean something — sabotaging
`bnode.intVal` changes all 12 files. The generator is genuinely reading through
the cursor, not quietly falling back.

Four kinds of site could not simply take `AnyNode`, and each is marked where it
sits rather than left for the next person to rediscover:

* THE GENERATOR REWRITES. `mAppendSeqElem`, `mNewSeq`, `genSetLengthSeq`,
  `genWasMoved` and `genArrToSeq` replace a child or a type IN PLACE, and
  `genEnumToStr`/`mAsgn`/`spawn` build fresh trees. Those run on `origin(n)` —
  the very node the buffer was encoded from — so the mutation lands exactly
  where it always did. Where the mutation is then READ (`genArrToSeq` retypes a
  bracket, `genArg` replaces a `var` param's type), generation continues on the
  origin too, because the buffer does not see the write and a cursor would keep
  reading the slot as encoded.
* NILABLE NODES stay `PNode`: a cursor has no standalone nil. That is the
  assignment DESTINATION throughout the call family (`genCall` passes nil), the
  `check` of an object-constructor field, `exvar`, `stepNode`, the `fin` of a
  try statement.
* `PNode`-KEYED TABLES AND ANALYSES take `origin`: `dataCache`, `isPartOf`,
  `lhsDoesAlias`, `potentialAlias`, the type-record walkers.
* SHARED PREDICATES in `ast.nim` cannot see `BNode`, so `skipHiddenAddr`,
  `isInfixAs` and `getStr` join `canRaise`/`getInt` as templates instantiated
  for both. `skipPragmaExpr` is a deliberate exception: it sits above the point
  in `ast.nim` where `firstSon` for a `PNode` exists, so `bnode` carries a
  one-line spelling with a pointer back.

Two Nim details worth recording. Repeated occurrences of a type class in one
signature share ONE implicit generic, so any proc whose two node parameters can
differ in representation needs explicit params — `genSingleVar`,
`genFieldObjConstr`, `callGlobalVarCppCtor`. And a `{.dirty.}` template inside a
generic resolves its identifiers at instantiation, so `genClosureCall`'s local
`rawProc` had to be bound before the template that uses it or it lost to the
module-level proc of the same name.

Verified: grind clean (1431 bodies, 260_431 nodes, 0 disagreements, origins
exact); the default path is byte-identical to HEAD; all four build
configurations compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 15:43:14 +02:00
Araq
e5bafa48c5 IC: make the location primitives representation-agnostic
`initLoc`, `fillLoc`, `putIntoDest` and `putDataIntoDest` take `AnyNode` and
store `origin(lode)`. That is the gate the previous commit was for: 99 of the
180 generator procs build a `TLoc` from their node, so until these four accept
a cursor none of those 99 could migrate, and with them accepting one they all
can — `TLoc.lode` is still a `PNode`, and on a bridged buffer it is the SAME
`PNode` a tree-driven build would have stored.

`bnode.origin` is the ambient accessor (through `currentNav`, like `sym` and
`typ`), so a generator proc does not have to be handed the buffer to build a
location. It answers nil for a file-backed body, which is correct — those tokens
came from no `PNode` — and asserts on a bridged one, where a node head always
has an origin and a miss means the cursor is not where the caller thinks.

The origin check in the grinder now goes through that ambient path rather than
calling `originOf` directly, because the direct call is not the path the
generator will take and testing it would have proved the wrong thing.

Verified: grind clean, origins exact by reference at every node of 1431 bodies;
the default path is byte-identical to HEAD; all four build configurations
compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 14:21:52 +02:00
Araq
9f34f5e42b IC: give the bridge origin tracking, so TLoc.lode stops blocking the generator
Measured before designing: of the 180 `PNode`-taking procs in `cgen`/`ccgexprs`/
`ccgstmts`/`ccgcalls`, **99 build a `TLoc` from a node**. So the generator cannot
move to the seam without an answer for `TLoc.lode`, and the obvious answers are
both bad — leaving it a `PNode` means a cursor-driven proc cannot fill it, and
changing its representation means editing `TLoc`, which lives in `astdef` at the
bottom of the module graph, pushing the seam far below the backend and forcing a
flag day.

There is a third answer, and nifcore already had the piece it needs.
`cursorToPosition` is documented as a stable per-token key, and `TokenBuf.len`
is where the next token lands — so the encoder records `position -> PNode` as it
walks, and `originOf` inverts it. A cursor-driven generator can then put the
ORIGINAL node into a location: `TLoc.lode` stays a `PNode`, and the identity
comparisons already in the backend (`preventNrvo`'s `dest != le`,
`isPartOf(d.lode, …)`) keep meaning what they meant, because it is the same
object and not an equal copy.

Asserted, not assumed: the bridge grinder now walks cursor and tree together and
requires `originOf(c) == a` by REFERENCE at every node — 1431 bodies, 0
failures. Recording the position one token off makes it fail on the first body,
so the check is not vacuous.

This unblocks the generator migration without a flag day: `expr` and its ~60
emitters can move to `AnyNode` with `TLoc` untouched.

Verified: grind clean (1431 bodies, 260_431 nodes, 0 disagreements); the default
path is byte-identical to HEAD; all four build configurations compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 14:15:37 +02:00
Araq
e988e0366c IC: hand the transformed body to cgen as a buffer, and read the analysis off it
`transf.handOffBody` is the seam between the two halves: rewriting ends, and
from there the reading side works off a cursor. `cgen.genProcLvl3` takes the
handoff once the body is final and runs `allPathsAsgnResult` — which has been
`AnyNode` since the predicates landed — over the CURSOR rather than the `PNode`.

Three things this had to get right, none of them obvious from the outside.

WHERE the handoff goes. A bridged buffer is a SNAPSHOT, so it has to be taken
after the last rewrite. Destructor injection runs after `transformBody` returns,
and `easyResultAsgn` sets `nfPreventCg` on the tree later still — a buffer taken
at the end of `transformBody` would describe a tree that no longer exists by the
time anything read it. So the API lives in `transf`, which owns that invariant,
and the call site is in `cgen`, which is where rewriting actually finishes. The
one flag `easyResultAsgn` writes afterwards is read only by the `PNode`
generator, and it sits in the branch that does not run the analysis at all.

WHY it is gated on `-d:newIcBackend`. `expr` cannot migrate a piece at a time:
it dispatches to ~60 emitters, so either all of them take `AnyNode` or the
dispatch converts at every node. Until that happens the generator still needs a
`PNode`, and building a buffer per routine in a default build would cost a tree
walk and buy nothing. The analyses are the part that moves now.

THAT IT IS ACTUALLY LOAD-BEARING. "Byte-identical output" is worth nothing if
the new path never ran, so: cursor-driven and `PNode`-driven builds produce
identical `.c` (50/50 on an 89k-line target, 12/12 on the grind target), and
forcing the cursor-driven answer wrong changes 10 of 12 files. The first number
alone would have been consistent with the code being dead.

Verified: grind clean (1431 bodies, 260_431 nodes, 0 disagreements, neither
tolerance taken); the default path is byte-identical to HEAD; all four build
configurations compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 14:04:37 +02:00
Araq
47144c7962 IC: a PNode <-> TokenBuf bridge, so rewrites stay on PNode
The generator does not migrate to a `Cursor` and should not: transf, destructor
injection, closure lifting and the tree codegen builds as it goes all CONSTRUCT
nodes, and a cursor is a read pointer into a shared token buffer. `nodebridge`
is the seam instead — a rewriting pass keeps producing a `PNode`, and anything
that only reads is handed a `TokenBuf`, from which a `Cursor` (and so a `BNode`)
is a pointer.

The bridge is NOT the `.bif` format, and the difference is the point. A `.bif`
is read by a different process, so every symbol and type has to be written as a
NAME to look up again. A bridged buffer is read by the process that built it, so
a symbol reference is `(bsym <idx>)` into a side table holding the very `PSym`
the encoder was handed, and the type slot is `(btyp <idx>)` the same way. The
node shape is otherwise identical to the file format, so `bnode` reads a bridged
buffer with the accessors it already has; `bodynav` grows one branch each in
`symAt`/`typeAt`, and `bnode` learns that `bsym` is an `nkSym`.

That buys the property this branch has been blocked on: **`sym` IS IDEMPOTENT ON
A BRIDGED BUFFER, FIELDS INCLUDED.** On the file path it cannot be —
`loadFieldStub` mints a fresh stub per use because two distinct fields can share
a name and a position across types — which is what stops `aliases.isPartOf`
moving to the seam. A bridge hands back the object it was given. The grinder now
asserts exactly that: field syms are excluded from the idempotence check on the
file path and INCLUDED on a bridged one.

Verification is the existing oracle pointed at a harder target. `grindBNode`
compares two decodings of one file and has to excuse two differences; the bridge
is compared against its own live input and must excuse NEITHER, so both
tolerances are counted and the bridge asserts it took neither. `toPNode` is
covered without a hand-written comparator that could share the encoder's bugs:
decode, RE-ENCODE, and grade the second buffer against the ORIGINAL tree, so
anything the decoder drops shows up as a disagreement.

Reach, measured: 1431 bodies and 260_431 nodes, against 782 and 67_857 for the
file path — the bridge sees every body, including the one-line `nkAsgn` ones
`ast2nif` never defers and the grinder therefore never saw. 0 disagreements.

Not vacuous: dropping node flags, perturbing the sym index and dropping the type
slot each make it fail immediately (`flags`, `sym identity`, `typ nil-ness`).

A bridged buffer must never be written to a file — `(bsym …)` means nothing
without the tables beside it. `ast2nif` remains the only serializer.

Verified: 215/215 byte-identical `.c` against HEAD on the default path; all four
build configurations compile, plus `nodebridge` standalone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 12:15:36 +02:00
Araq
bca07265a4 tests/ic: say how to run the suite, and how to tell slow from hung
The metamorphic tests compile the program twice per step — once under `nim ic`,
once as the `nim c` reference — which is 100+ compilations for the category,
each fanning out a compiler process per module per stage. On a machine short of
RAM that swaps, and the symptoms read exactly like a deadlock: processes at 0%
CPU, no output, a different test "stuck" every run, and the same compilation
finishing in seconds standalone.

It is not a deadlock. A `nim ic` parent at 0% CPU is waiting on its children,
and the test name does keep changing if you watch long enough. Writing this down
because it has now been misdiagnosed as a testament/`nim ic` interaction more
than once, by me, before I measured `vm_stat` and `vm.swapusage`.

Also records the fan-out caps (`--parallelBuild:N`, `-d:icJobs:N`,
`-d:icNoParallel`), that serial mode is what makes per-process diagnostic output
readable rather than interleaved, that `testament r` cannot run a multi-step
metamorphic file, and that `*_temp.nim` are gitignored scratch rather than tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 11:15:21 +02:00
Araq
c1a815fa07 IC: take the last leaf readers, and map where the leaf migration ends
`skipAddr`, `skipAddrDeref` and `isInactiveDestructorCall` move to the seam —
all three pure, none of them touching a field symbol. The node-returning ones
are graded by a `checkNodeResult` template rather than a hand-written pair, so
adding the next one is a line.

The more useful half of this commit is the map. Counting rather than guessing:
of the 191 `PNode`-taking procs across `cgen` and the `ccg*` files, 160 EMIT —
they take a `Builder`/`TLoc` out-param or write into `p` directly. Those do not
move one at a time. They are one mutual recursion rooted at `expr`/`genStmts`,
so the generator moves as a unit or not at all, and doing that needs write-side
capability the seam does not have.

What is left over is not a backlog, it is six named blockers, and `bnode`'s
module doc now says which proc each one holds up and why:

* a type's RECORD TREE is not a body (`PType.n` stays `PNode` by design, so
  `asgnComplexity` and friends were never candidates);
* RETURNS A NODE OR NIL (`getPragmaStmt` — no nil token exists to return);
* WRITES TO THE NODE (`easyResultAsgn` sets `nfPreventCg`; the seam is
  read-only);
* NEEDS RENDERING (`preventNrvo` interpolates `$le` into a warning);
* NEEDS STABLE FIELD IDENTITY (`lhsDoesAlias`, `potentialAlias`, through
  `isPartOf` — see the `sym` note, this one is blocked on a property rather
  than on effort);
* MIXED REPRESENTATION (`potentialAlias`, `getPotentialReads` carry a
  `seq[PNode]` beside the node).

Stating it this way because the alternative is someone re-deriving each blocker
by trying the migration and watching it fail, which is how three of the six were
found.

Verified: grind clean (67_857 nodes, 0 disagreements); 215/215 byte-identical
`.c` against HEAD on the default path; all four build configurations compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 11:06:52 +02:00
Araq
fe39ad5fba IC: sym is not a function of its argument for fields — find out, then say so
The goal was `aliases.isPartOf`, the deepest thing on the migration list:
mutual recursion over two trees at once, reaching `sym`, `typ`, `intVal` and
`isDeepConstExpr`. It does not migrate, and the reason is worth more than the
migration would have been.

`bnode.sym` IS NOT IDEMPOTENT for object fields. Two calls on the SAME token
yield two different `skField` `PSym`s with consecutive item ids — field uses
bypass the nav's memo and go to `loadFieldStub`, which mints per use because
two distinct fields can share a name AND a position across types, so one shared
stub would mistype one of them. `isPartOf` compares `a[1].sym.id != b[1].sym.id`
to decide whether two accessor chains touch the same field, so on a cursor it
answers `arNo` where the AST answers `arYes`: wrong alias analysis feeding NRVO
and observable-store decisions.

The grinder caught it on the first run after the migration, on a self-comparison
`isPartOf(n, n)` — a shape production never passes, which is exactly why it was
worth grading. The property was then confirmed directly rather than inferred:
call `sym` twice on one token and print both, and the ids differ.

So `aliases.nim` is reverted, and what stays is the knowledge:

* `bnode.sym` says which symbols it is stable for and which it is not, what
  codegen actually consumes for a field instead (the name it re-navigates the
  reclist with, plus the position for tuples — the same tolerance the grinder
  applies), and why this module cannot fix it alone: a stable field identity
  needs the token's own position as a key, and `nifcore.Cursor` keeps that
  pointer private.
* The grinder asserts idempotence for every NON-field sym at every node. It
  passes, and admitting fields makes it fail immediately — so the check states
  a precise boundary rather than a vague warning, and the day fields join it
  will be visible.

`getInt` moves into a template so `bnode` can instantiate it (the `canRaiseImpl`
pattern), and `astalgo.sameValue` becomes `AnyNode` and is graded — both are
literal-only and unaffected by the field problem. `preventNrvo` records the
OTHER kind of blocker while it is fresh: its alias analysis is fine, but the
`warnObservableStores` message renders the node, and rendering is a capability
the seam does not have at all.

Verified: grind clean (67_857 nodes, 0 disagreements); 215/215 byte-identical
`.c` against HEAD on the default path; all four build configurations compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 10:41:05 +02:00
Araq
06b1bf8f9a IC: take the seam into trees and ccgutils
`trees.nim` can import `bnode` — nothing in `bnode`'s import closure reaches
`trees`, checked rather than assumed — so the shared helpers move to `AnyNode`
instead of being reimplemented behind the seam: `getMagic`, `whichPragma`,
`getRoot`, `isDeepConstExpr`, plus `ccgutils.stmtsContainPragma`. That unblocks
three more codegen procs, `canMove`, `notYetAlive` and `ifSwitchSplitPoint`,
which needed them and nothing else.

`stmtsContainPragma` could not simply stay `getPragmaStmt(n, w) != nil`, and
the reason is worth recording because it will recur: a proc that returns a node
OR NIL is the one shape the seam cannot serve. `.bif` spells a missing child as
a `DotToken` *inside* a tree; there is no nil token to hand back as a return
value and a `Cursor` is not nilable. So the predicate is split out — and,
because that leaves two copies of one traversal, `grindPredicates` now asserts
the two agree at every node instead of trusting them to.

Measuring the answers, not just the agreement, again earned its keep. Six of
the new checks came back with a wide spread (`getMagic` 7780 non-`mNone` over
many magics, `getRoot` 19506 non-nil syms compared by identity, `isDeepConstExpr`
7917 true, `notYetAlive` 9653 true). Two came back CONSTANT — `stmtsContainPragma`
false at all 67_721 nodes and `ifSwitchSplitPoint` zero at all 24 — because
nothing in the closure uses `{.linearScanEnd.}` or `{.computedGoto.}`. Both are
now exercised on both answers by shapes added to `tools/icgrind`. A check that
grades a constant is indistinguishable from a passing check in the output, so
this only shows up if the distribution is looked at.

Verified: grind clean over the whole `--ic:on` closure (67_857 nodes, 0
disagreements); the target's `--ic:on` output matches its `nim c` output;
215/215 byte-identical `.c` against HEAD on the default path; all four build
configurations compile.

Sabotaging `bnode.secondSon` — an accessor the lockstep walk does NOT itself
use, since it descends by index — is caught only by this layer, and is: it
fires on `getRoot`, `isDeepConstExpr`, `reifiedOpenArray` and
`skipTrivialIndirections`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 10:23:43 +02:00
Araq
837082eb89 IC: grade every migrated predicate at every node, and migrate nine more
Nine `PNode`-typed codegen procs become `AnyNode`, all pure readers:
`bodyCanRaise` (the consumer of the `canRaise` work), `isAssignedImmediately`,
`branchHasTooBigRange`, `hasNoInit`, `reifiedOpenArray`,
`skipTrivialIndirections`, `isSimpleExpr`, `isConstClosure` and `fewCmps`.

The signatures are the small part. A migrated proc that nothing calls with a
`BNode` is not even type-checked, so the substance is `grindPredicates`: every
one of them runs on BOTH spellings of the SAME node, at EVERY node of every
graded body, inside the walk `grindLockstep` was already doing. 67_721 nodes
on the reference target, 0 disagreements.

Two things had to be gated, and neither by widening a guard until the run went
green.

The tolerated `(ht . <sym>)` type difference is benign for the vocabulary check
and NOT benign for a predicate that reads `typ` — and because the predicates
recurse, one excused node poisons every ancestor's answer too. `grindLockstep`
now reports whether a subtree is free of it, and only clean subtrees are
graded.

`isAssignedImmediately` and `fewCmps` hand `n.typ` to `getSize`/`mapType`,
which are total only over types the C backend can lay out. Asked at an
arbitrary node they meet a `tyGenericParam` or a `tyAnything` and abort — that
is a question with no answer in either spelling, not a disagreement between
them. Both are graded FROM THE PARENT, at the position production calls them
from. Declarative subtrees are skipped for the same reason, along the boundary
`bodyCanRaise` already draws.

Both exclusions are counted and printed beside the graded count, so a run that
grades nothing cannot pass for a run that grades everything.

`tools/icgrind` versions the grind target, because two ways of silently
getting no coverage turned up while writing it: the main module's routines are
never graded, and `ast2nif` defers only `nkStmtList` bodies, so a one-line
`proc f(x: int): int = case x ...` is invisible to the oracle. Shapes added
for `branchHasTooBigRange` and `fewCmps` produced exactly zero coverage until
both were found by counting rather than assumed.

Verified: 215/215 byte-identical `.c` against HEAD on the default path;
sabotaging `isAtom` and sabotaging the parent-driven node selection each make
the grinder fail on the first body it reaches.

Recorded rather than papered over: `isConstClosure` is graded only on its false
side — the whole closure contains one `nkClosure` node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 10:08:07 +02:00
Araq
512d2a8f26 IC: revert the (ht . <sym>) lazy-type pin — it broke sem
The previous commit made `(ht . <sym>)` — a sym node the writer gave an
EXPLICITLY nil type — load back with `nfLazyType`, so `ast.typ` answered
`sym.typ` instead of nil. The stated reason was to remove a load-order
dependence, and the direction was wrong: that nil is load-bearing.

`writeSymNode` only emits the wrapper when the node's own type DIFFERED from
its symbol's, so a nil there says the node genuinely had no type while the
symbol had one. A type symbol used as a VALUE is exactly that shape:
`newException(KeyError, ...)` passes a typedesc, whose node carries no type
while the symbol carries the object type. Handing it `sym.typ` makes sem read
the typedesc as an expression of the type it denotes, and `--ic:on`
compilation of anything instantiating `tables.[]` dies with "only a 'ref
object' can be raised". A four-line program is enough:

    import std/tables
    var t = initTable[string, int]()
    t["a"] = 1
    echo t["a"]

The load-order dependence is real but is not fixed by pinning the flag EITHER
way — setting it breaks sem as above, clearing it would strip the fallback
from the not-yet-loaded-stub population that `nifcBackendActive` exists to
serve. Left alone deliberately, with the reasoning recorded at the site.
`bnode.typ` answers the faithful nil, and the grinder excludes this one shape
via `hasExplicitNilType` — narrowly, only when the cursor says nil and the AST
is saying exactly the symbol's type.

Why the suite did not catch it: `tests/ic` passed 39/39 throughout. The same
four-line program reproduces from the scratchpad and from the repo root, and
PASSES under `tests/ic` — `--skipParentCfg --skipProjCfg` makes it fail there
too, so `tests/config.nims` is what masks it, most plausibly because
evaluating a NimScript config runs the VM and perturbs the very load order the
bug depends on. A test file under `tests/` therefore cannot guard this class,
and no test is added rather than one that passes on the buggy compiler.

Also in this commit, and the reason the bug was found at all:

* `effectsOf` / `raisesNothing` replace the raw subscripting of `fn.typ.n` in
  `canRaiseImpl`, so the effect-list layout is written down in one place and
  the templates carry no knowledge of it. `raisesNothing` is stated as the
  NEGATIVE on purpose — the safe default is "can raise", so the one narrow
  shape that licenses dropping an exception check is the one spelled out, and
  an unanticipated shape falls conservative by construction.

* `-d:icCanRaiseLog` logs every `canRaiseDisp` verdict keyed by name, disamb
  and OWNING MODULE, with the deciding branch. What "the canRaise helpers work
  on a `.bif`" means is that the type the decoder materialises carries the same
  effect list the from-source one did — a claim about the WRITER that the
  BNode/PNode grinder structurally cannot make, since both spellings ask the
  same `PType` and agree however wrong it is. The only oracle is the same
  program built without IC: 234 callees comparable, 0 disagreeing, 23 of them
  reaching the effect-list branch in both builds.

  Two instrumentation bugs worth recording, because both produced confident
  wrong numbers first: keying by name+disamb alone collided (`len.0` names a
  different routine per module) and reported one false disagreement; and the
  branch marker was a global that `canRaiseDisp` left stale on its early
  return, which inflated effect-list coverage from 23 to a claimed 142.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 08:54:46 +02:00
Araq
5799220c98 IC: grind the Cursor vocabulary against the PNode loader, and fix what it found
The `BNode` accessors had no oracle. The self-test in `bnode.nim` checks them
against each other and against the raw token stream, which a uniformly wrong
vocabulary satisfies — it passed 5.2M assertions on an off-by-two model. This
adds the missing oracle and lets it drive the next migration step.

`cgen.grindLockstep` (opt-in, `NIM_IC_BNODE_GRIND=1` on an `--ic:on` build)
walks the `.bif` cursor and the materialised `PNode` for the SAME body side by
side and requires `kind`, `len`, `info`, `flags`, the literal payloads, `sym`
and `typ` to agree at every node of every routine body in the dependency
closure. The `PNode` is the oracle, so it compares everything rather than what
someone thought to check. It found two bugs, both silent:

* `BNode.typ` answered `nil` for every bare `Symbol`. `ast.typ` does not: it
  falls back to `n.sym.typ` when the loader set `nfLazyType`, which it does for
  exactly that shape. The consequence is not less information but a DIFFERENT
  answer — `canRaise` asks `fn.typ.kind == tyProc` about a call's callee, so
  nil turns "this call can raise" into "it cannot" and drops the
  goto-exception check after the call.

* `(ht . <sym>)` — an explicitly nil node type — made `n.typ` LOAD-ORDER
  DEPENDENT in the loader itself. `newSymNode` marks the node lazy only if the
  symbol was still an unloaded stub at that moment, so the same `.bif` node
  answered `sym.typ` or `nil` depending on what happened to touch that symbol
  first. Pinned to the lazy reading, so the answer is a property of the file
  rather than of the traversal order.

With `typ` correct, the blocker recorded in `allPathsAsgnResult` is gone.
`ast.canRaise`/`canRaiseConservative` cannot become `AnyNode` procs where they
live — `BNode` is defined in `bnode.nim`, which imports `ast` — so their bodies
move into templates that `bnode` instantiates for its own node type. One source
of truth, no cycle, no second copy. `ccgcalls.canRaiseDisp` and
`cgen.allPathsAsgnResult` follow.

Adds the leaf accessors (`intVal`, `floatVal`, `strVal`, `ident`, `flags`)
because nothing in `ccgexprs` can migrate without them, and `rawDesc` for
diagnosing a disagreement in terms of what the token stream literally says.

`compiler/bodynav.nim` replaces the `BodyScope` snapshot with a scope chain the
traversal maintains — `openScope`/`closeScope`/`registerDefHere`, lookup
falling through to the decoder — ported from Nimony's `typenav`. The scope
becomes a product of the walk, so nothing is copied ahead of time and nothing
can be stale. How much of a live problem the snapshot was is measured rather
than assumed: `-d:icLocalSymStats` reports `localHit=0 fieldStub=2 miss=0
sdReg=5902 extractReg=45` over the stdlib closure, i.e. definitions register
constantly and not one use ever resolved through the table, because
`isLocalSym` is a hardwired `false`. The hazard was latent; this keeps it
latent once that stops being true.

Verified: 0 disagreements over the whole `--ic:on` closure with the walk
driving the nav; deliberately breaking `intVal`, `flags` and the nav key each
make the grinder fire on the first bodies it reaches, so the clean run is not
vacuous; the default path emits 215/215 byte-identical `.c` against the
pre-change compiler and does not compile `bodynav` at all; `tests/ic` 39/39.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEF7FJvUkGKvG9LSGuEaNR
2026-08-30 08:07:38 +02:00
ringabout
dcec8e1cd1 fixes #26134; del(seq) performs self-assignment and =destroy for del(… (#26138)
…0) of 1-length seq


fixes #26134
2026-08-29 14:41:36 +02:00
Ryan McConnell
8cb406cd7a Fix 26144; exception propagation for non-raising virtual methods (#26145)
ref #26144 

The C backend must not use `sfNeverRaises` to remove exception checks
from
virtual method calls. The flag describes only the selected base method
body,
while a vtable override may raise a catchable exception.

This change makes `canRaiseDisp` conservative for `skMethod` symbols and
adds a
regression test covering an exception raised by a child method invoked
through a
base reference.
2026-08-29 14:41:05 +02:00
ringabout
802bcf5a2d fixes #26132; =destroy should accept non-parametrized generic (#26142)
fixes  #26132
2026-08-29 14:40:46 +02:00
araq
2447dfdc7d cgen: name the PType child instead of subscripting it
The same treatment the `PNode` side just got, for the reason that applies to
types: `t[0]` is the return type, the base class, the index type or the generic
head depending on the kind, and the subscript says none of that. Every child
access in the cgen files that has a named accessor now uses it — `baseClass`
for the eleven object-hierarchy walks, `elementType` for the seq/openArray
element, `returnType`, `genericHead`, `firstGenericParam` — and the two loops
that walked a type's children become `paramTypes` and `kids`.

Left indexed on purpose: a parameter reached by ARGUMENT position
(`typ[i]` in ccgcalls/ccgstmts), a tuple field, and a generic parameter at an
explicit index. There the index is the clearest thing to write.

Every substitution is exact rather than merely close. `[]` with index 0 is
unconditionally `sonsImpl[0]`, so `baseClass`/`returnType`/`genericHead` cannot
diverge; `elementType` is `sonsImpl[^1]` and is used only where the type has a
single son; `paramTypes` and `kids` are literally the loops they replace.

`ast.sons(t: PType)` gets the warning it has been missing. Despite the name it
is not the counterpart of the `sons` ITERATOR over a `PNode`: it returns the
raw seq, and a `tyProc` keeps its parameter types in `n`, so that seq holds
only the return type while `[]`/`len`/`kids` route parameters through
`n[i].sym.typ`. `for x in t.sons` therefore compiles, reads exactly like the
`PNode` idiom, and visits a different set of types — which is what
`ccgutils.encodeType` would have started doing had it been converted to `sons`
rather than `kids`. Marking the proc deprecated and rebuilding shows one call
site in the whole compiler (`previouslyInferred`), so the trap is latent, not
active.

`bnode.nim` also records that there is deliberately no `BType` beside `BNode`:
types stay `PType`s under `newIcBackend` — `BNode.typ` returns one — because
the backend asks them questions (`skipTypes`, `getSize`, `lengthOrd`, the
record walk over `t.n`) that a raw cursor cannot answer.

Pure refactor: all 216 generated `.c` files byte-identical to the parent commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-29 09:05:44 +02:00
araq
437876efbd IC: say what BNode.sym/typ/info actually need
The stubs guessed. Checking `ast2nif` shows all three need a resolution
context and so cannot stay unary accessors on the cursor:

  * a `Symbol` token holds only a NAME; `loadSymStub` resolves it from a
    `DecodeContext` plus the owning module plus that routine body's `localSyms`,
    because an unsuffixed name is body-local and is in no index;
  * `createTypeStub` likewise takes a `DecodeContext`;
  * `rawLineInfo` yields a `NifLineInfo` in the `.bif`'s own file pool, which
    `oldLineInfo` maps to a `TLineInfo` through a `LineInfoWriter` holding the
    `ConfigRef`.

So the open question is not how to write these three but where codegen gets
that context — a parameter, or module-global state for the span of one module's
`cg` stage, as `ast2nif` already keeps for its writer. Recorded so it is not
rediscovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-29 07:10:59 +02:00
araq
20b70c8b1e IC: implement the Cursor half of the codegen vocabulary
`BNode`'s `Cursor` branch stopped at `{.error.}` stubs. Everything structural
now works, so `-d:newIcBackend` no longer fails on the shape of the tree — the
remaining stubs (`sym`, `typ`, `info`) are the three that need the decoder's
symbol/type/line-info maps, which is the honest next boundary.

Two corrections to the seam, both found by looking at what actually reads a
`.bif`:

  * it was pointed at `nifcursors`, the WRITER cursor over `PackedToken`s.
    `bif.load` produces a `nifcore.TokenBuf` and `ast2nif` decodes it with a
    `nifcore.Cursor`; that is the type the backend will get.
  * the cost model said reading child `i` is O(size of children 0..<i). It is
    O(i): a `TagLit` token stores the width of its whole subtree, so
    `nifcore.skip` is one pointer add no matter how big the subtree is. Indexed
    loops are still quadratic and still worth removing, but in the number of
    children, not in tree size.

`kind` is the accessor the stub called pivotal, and the note on it was wrong in
a way that matters: a `.bif` carries its OWN tag pool, so a tag id means
nothing outside its file and the "build a tag-id -> TNodeKind table once" plan
cannot work. It memoizes per pool and drops the memo when the pool changes.

Since no call site executes any of this yet, `when isMainModule` walks real
`.bif` files and checks the vocabulary against itself and against an uncached
tag lookup. Over 70 files (`.s.bif`, `.t.bif`, `.iface.bif`) from the
testworkspace corpus: 2.05M nodes, 9.19M assertions, all passing. Both halves
of the harness were confirmed live by sabotage — `secondSon` returning child 2
trips it, and so does dropping the tag-pool memo invalidation, which is what
proves ids really do differ between files.

The default build is untouched: all 216 generated `.c` files still byte-
identical to the parent commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-29 07:09:23 +02:00
araq
9e076d74c0 cgen: no [] on a PNode outside tree construction
Completes the vocabulary migration started with `BNode`: every child READ in
the cgen files now goes through an accessor that a `.bif` `Cursor` can also
serve, so flipping `newIcBackend` is a matter of implementing the vocabulary
rather than rewriting call sites.

  * constant indices -> `firstSon` / `secondSon` / `lastSon` / `son(n, k)`,
    including the `namePos`/`paramsPos`/`bodyPos`/... slot reads;
  * indexed loops -> `sons` / `sonsFrom` / `sonsButLast` and the index-yielding
    `isons` / `isonsButLast`. `for i in 0..<n.len: n[i]` is quadratic once
    `BNode` is a `Cursor`, because reaching child `i` costs one `skip` per
    preceding SUBTREE;
  * `n.len == 0` / `> 0` on a node -> `hasSons`, which does not count.

`astdef` gains `sonsButLast(n, count)` and `isonsButLast` — the
`nkOfBranch`/`nkExceptBranch` shape, whose last child is the branch body, and
with `count = 2` the `nkVarTuple`/`nkIdentDefs` shape.

Three places needed more than a rename:

  * the C++/goto/setjmp try generators re-subscripted `t[i]` up to ten times
    per iteration of their `while i < t.len` walk; the branch node is now
    hoisted once per step;
  * `genParams` scans the arguments BACKWARDS to decide which need a temporary,
    which a `Cursor` cannot do at all. It materializes them in one forward pass
    and indexes that — the same order of work, since `needTmp` already
    allocates per call;
  * loops that stop at a computed position (`casePos`, `until`, `splitPoint`)
    walk forward and break instead of counting up to the bound.

What is left is exactly what a `Cursor` backend will not do: writes that build
a fresh `nkProcDef`, and subscripts of a `PType`, `string`, `seq` or `Table`.
`bnode.nim` records the invariant and the `PType` trap — `ast.sons(t: PType)`
is a proc returning `var TTypeSeq`, not the iterator of the same name — which
the type checker enforces, since the `firstSon`/`secondSon`/`lastSon`/`son`
family exists for `PNode` only.

Pure refactor, verified as one: all 216 generated `.c` files byte-identical to
the parent commit; metamorphic IC 16/16; icSuite 19/19 fragments; categories
gc 78, arc 140, destructor 97, closure 23, iter 71, trmacros 6, cpp 50,
exception 47, casestmt 16 with one pre-existing environmental failure
(tests/cpp/tasync_cpp.nim: `cannot open file: jester`).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
2026-08-29 06:57:55 +02:00
araq
8d4ddd5516 IC: introduce BNode, the seam for running codegen off a .bif Cursor
Building `PNode` trees out of a module's `.bif` is the bulk of the `lower` and
`cg` stages: their cost tracks the size of the dependency CLOSURE a stage loads,
not the module it compiles (a 370-byte module costs 0.20s/0.16s in lower/cg, the
main module 3.40s/3.16s). Since the link stage stopped loading the graph those
two are ~85% of the serial backend critical path. The goal is to let the backend
read the `.bif` directly through a `Cursor`, constructing no `PNode` at all.

`BNode` is the seam: a `Cursor` with `-d:newIcBackend`, a `PNode` without, which
is what every build does today. Codegen migrates one area at a time and the
compiler keeps building throughout.

The seam is thin on purpose. `ast`/`astdef` already give `PNode` the whole
vocabulary — `kind`, `len`, `safeLen`, `sym`, `typ`, `info`, `firstSon`,
`secondSon`, `lastSon` and `sons`/`isons`/`sonsFrom` — so bnode deliberately does
NOT redefine them for `PNode`: an identical second overload makes every call site
ambiguous (tried it; `lastSon` breaks first). It adds only `son` and `hasSons`,
and supplies the full vocabulary on the `Cursor` side as `{.error.}` stubs, so
flipping the define names the exact missing accessor AT ITS CALL SITE instead of
collapsing into a cascade of type errors.

Two notes for whoever continues this:

* The migration front is SIGNATURES, not call sites. Converting `n[i]` to the
  iterator vocabulary changes nothing while the value is still `PNode`-typed —
  flipping the define then reports a plain type mismatch. Changing one proc's
  parameter from `PNode` to `BNode` is what moves it, and is a no-op with the
  define off. `containsResult` and `allPathsAsgnResult` are converted as the
  worked example.
* `BNode`'s cost model differs and the vocabulary is shaped around it: reading
  child `i` of a `Cursor` is O(size of children 0..<i), so prefer `sons`/
  `sonsFrom`/`isons` over indexing, and avoid `len` in a loop condition.

Note `type BNode = when defined(x): Cursor else: PNode` does not parse — `when`
is not an expression in type position; it needs a `when` block over two `type`
sections.

Inert: all 219 generated `.c` files and the linked binary are byte-identical to
the parent commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 05:44:39 +02:00
araq
f84f53bff4 cgen: replace indexed child loops with the sons/isons/sonsFrom iterators
`for i in k..<n.len: ... n[i] ...` is the dominant shape for walking a `PNode`'s
children in the code generator: 41 such loops across the cgen files, and 777
indexed node accesses in total. It reads worse than iterating, it bounds-checks
every subscript, and it is quadratic the moment the backend reads children off a
NIF `Cursor` rather than a materialised tree (a child is `firstSon` plus one
`skip` per preceding sibling, and `skip` steps over a whole subtree).

31 of the 41 are converted:

* 20 to `sons`/`sonsFrom` — the index only ever subscripted `n`.
* 9 to `isons`, which now takes a `start` index (defaulting to 0, so its eight
  existing call sites are unchanged). These genuinely need `i`: a parallel index
  into the routine's `PType` (`typ.n[i]`, `typ[i]`), a `needTmp[i-1]` lookup, an
  `i == field.position` test, `$i` in a generated struct name, or the index
  passed straight to `genOtherArg`.
* 2 to `sonsFrom` with a variable start (`firstParam`, `offset`).

`sonsFrom` is new, next to `sons`/`isons` in astdef.

The remaining 10 are deliberate. Eight are not `PNode` at all — `varargs[Snippet]`,
`seq[PSym]`, `string`, and `PType`, where `sons` is a `proc ...: var TTypeSeq`
rather than an iterator, so a blind rewrite would compile into something quite
different. Two iterate `0..<it.len-1`, excluding the last child, which no
iterator expresses cleanly.

Pure refactor, and verified as one: all 219 generated `.c` files of a 219-module
program and the linked binary are byte-identical to the parent commit. That is
the bar that matters here, because the index arithmetic (`i-1`, `i == position`,
`$i`) is the easy thing to get wrong. It also caught a real slip on the way:
`genFieldCheck` reassigns its loop variable, which a `for` binding cannot do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 05:44:14 +02:00
araq
e0c0724b62 IC: the link stage no longer loads the module graph
`--icBackendStage:link` called `loadBackendModules` — a whole-program
deserialization — for exactly two things: each module's `.c` path via
`getCFile`, and its recorded C compile/link directives via
`replayBackendActions`. That was 3.7s of the ~11s serial backend critical path
on a 219-module program, spent recovering a list of paths and a handful of
strings.

Both now come from artifacts the earlier stages already produce:

* `.c` paths from the driver's existing `LiveModulesFile` manifest (which
  `merge` already reads); the `.c` sits beside each listed `.c.nif`, so no new
  manifest was needed. The merge-decision fallback for conditionally-imported
  nodes that own a live symbol is kept.
* C directives from a new `<module>.c.cflags` sidecar. The module's own `cg`
  already replays them, so it writes them down too — one tab-separated line per
  `compile`/`link`/`passl`/`passc`/`localpassc`/`cppdefine` action. `localpassc`
  needs the module's source path, which only the writer can resolve, so it is
  baked into the line. Written unconditionally, empty included: it is a declared
  nifmake output of the `cg` rule and a missing output re-fires the rule for
  ever.

link: 3.70s -> 0.44s. A one-line code edit on the corpus goes 15.2s -> 11.8s;
cold 124s -> 120s; no-op unchanged at 0.27s.

Green: 16/16 metamorphic IC tests, the 17-file `koch ic` suite, 13/13
differential edit checks against `nim c`, and `bootic` iteration 1 (the full
fixed-point check was still running when this was committed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 23:13:16 +02:00
araq
d81e764f98 IC: the cg stage must not write emit's .c
`registerModuleCode` writes the module's `.c` and registers it for compilation.
Under `--icBackendStage:cg` it must do neither: `cg`'s product is the `.c.nif`,
the `.c` is rendered by `emit` from that artifact plus the GLOBAL merge
decision, and the compile registration belongs to `link`.

Writing it in `cg` puts a second, differently-filtered `.c` — `cg` can only
filter by the liveness its own process sees — at the exact path `emit` declares
as its nifmake output. Two stages claim one output, and the `.c` ends up newer
than `emit`'s own `.c.nif` input, so nifmake considers `emit` up to date and
skips it: `cg`'s unfiltered text goes to the linker and every emit-everywhere
body is defined twice ("multiple definition of eqdup__u727466987__OOZ…").

Nothing surfaces this today because `merge` rewrites the decision file on every
run and every `emit` lists it as an input, so all of them re-fire and overwrite
the stray file. The fire-all is therefore load-bearing, not the "insurance" it
is documented as — and it is what makes a content-stable merge decision
(219 fewer processes and ~13s less backend CPU per edit on a 219-module
program) impossible: that experiment is exactly how this was found.

No behaviour change on its own, since the fire-all still runs. Green: `bootic`
fixed point, 16/16 metamorphic IC tests, the 17-file `koch ic` suite, and 13/13
differential edit checks against `nim c`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 22:38:14 +02:00
Constantine Molchanov
f897fe8c29 Support :code: argument in .. include:: directive. (#26146)
This is part of the reST spec, useful for code snippet inclusion:
https://docutils.sourceforge.io/docs/ref/rst/directives.html#include
2026-08-28 22:34:18 +02:00
Ryan McConnell
33ee586913 fixes #11797; fix C type hashes for imported aliases (#26150)
Fixes #11797.

Imported scalar and pointer aliases inherit their external C spelling,
but
receive a different Nim symbol. Signature hashing previously used that
symbol
identity, so aliases that emit exactly the same C type could produce
different
  backend names for tuples, sequences, and other generic types.

  For example, `cint` and `type CIntAlias = cint` both emit `int`, but
`seq[cint]` and `seq[CIntAlias]` could be emitted as incompatible C
structs.
The Nim type checker nevertheless permits assignments and calls between
them,
  causing the generated C or C++ compilation to fail.

This changes the backend hash to use the external type spelling when
available.
A symbol-based fallback remains for imported types without a resolved
spelling.

The change deliberately does not collapse imported types into their
underlying
Nim builtin. Types such as `pid_t`, imported pointers with qualifiers,
and
  platform typedefs may require distinct backend representations.

  ## NIF and incremental compilation

This does not change NIF serialization, NIF type keys, or the IC cache
format.
The bug is in backend type-name generation. An IC regression test is
included
to ensure that the corrected backend identity is preserved when
compilation
  passes through the NIF pipeline.

  ## Tests

  The regressions cover:

- tuple and sequence assignments between an imported type and its alias
  - cross-module sequence parameters and mutation
  - C and C++ backends
  - NIF-backed incremental compilation

  Existing C-type tests were also run under C/C++, refc, and ARC.

  ## Remaining scope

This does not solve the broader question of compatibility between
imported and
  builtin types that have different backend identities, such as
  `seq[cdouble]` and `seq[float]`. That remains tracked by #19374.
2026-08-28 22:33:20 +02:00
Ryan McConnell
0be9b4f3f6 fix 26147; new-style concepts: broken generic (Case B) (#26151)
ref #26147
2026-08-28 22:31:58 +02:00
ringabout
c36c527db3 fixes #26143; Possible memory error (#26154)
fixes #26143

follows up https://github.com/nim-lang/Nim/pull/20307
2026-08-28 22:26:18 +02:00
araq
3c53629164 IC: don't build interface tables for dep-of-a-dep module loads
`loadTransitiveHooks` loads a module only to register its hooks, macro-cache
replay and generic-instance offers. It is a dep-of-a-dep, not an import, so
none of its symbols are visible to the module being semchecked — yet it went
through the full `loadNifModule`, which builds the interface string tables by
calling `loadSymFromIndexEntry` on every index entry, and `processTopLevel`,
whose `export` branch calls `resolveSym` on every exported symbol. Both write
into an `interf`/`interfHidden` pair that is scratch, shared across all
iterations, and never read. Their only other effect is warming the name-keyed
`c.syms` cache, which `resolveSym` refills lazily from the same index on a miss.

New `LoadFlag.SkipInterfaceTables`, set only by `loadTransitiveHooks`, gates
both. Measured on a 219-module program: a one-line edit rebuild goes 5.58s ->
3.21s (-42%), and the compiler's own cold IC build 202s -> 164s (-19%), since
every `nim m` in a cold build pays this too. Profile of the `nim m` before:
direct imports 17 modules/1.40s, transitive hooks 209 modules/2.99s,
re-exports 39/0.23s.

This is NOT byte-inert, unlike the previous commit: skipping the eager stub
creation shifts the per-module load-order counter, so lazily-created stubs get
different serialized item ids and 44 of 238 `.s.bif` change. The artifacts stay
deterministic (238/238 reproducible across two independent cold builds) and
16/16 metamorphic tests pass, including `clean cache == incremental cache` and
`a no-op edit changes no artifact` — so an existing nimcache re-sems those
modules once and then converges. Also green: the 17-file `koch ic` suite, 13/13
differential edit checks against `nim c`, both `bootic` fixed points (debug and
release), and 482 tests across tests/arc, destructor, closure, iter, gc, async
with a failure set identical to the parent commit's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 20:03:42 +02:00
araq
1f504865ed IC: one definition of a backend-minted symbol's disambiguator
The rule deciding which integer identifies a backend-minted symbol —
content-derived `disamb` for a lifted hook (`setHookDisamb`), `itemId.item`
otherwise — was written out at three sites: `mangleProcNameExt` and
`ccgutils.makeUnique` for the C name, `ast2nif.toNifSymName` for the NIF name,
each carrying its own copy of the ten-line rationale.

They drifted, which is exactly cce17461d: `toNifSymName` lacked the hook
exception, so the loader overwrote a content-derived value and two unrelated
`=destroy` hooks collided on one C name (C accepted the mistyped call, C++
rejected it). `astdef.backendMintedDisamb` is now the single definition and all
three call it. `globalName` still reads `disamb` directly — correct for a loaded
symbol, and the round-trip invariant that makes it agree is now stated in the
shared function instead of left implicit.

Pure de-duplication, verified as such: all 219 generated `.c` files of a
219-module corpus and the linked binary are byte-identical to the previous
commit. Plus 16/16 metamorphic IC tests, the 17-file `koch ic` suite, 13/13
differential edit checks against `nim c`, and the debug and release `bootic`
fixed points.

Note for the record: this started as an attempt to replace the per-process
`_c<itemId.item>` counter with a content hash. Instrumenting both mangling
sites showed that branch is never taken — 0 of 166 backend-minted manglings on
the corpus, 0 of 257 on the compiler, all going through the content-derived
path — so rewriting the scheme would have shifted every backend-minted C name
and forced an `icFormatVersion` bump for no demonstrable benefit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 20:03:14 +02:00
araq
0c0cc1e496 remove owner-checking-tower 2026-08-28 15:12:56 +02:00
araq
1387093f99 don't redo nifler steps unnecessarily 2026-08-28 14:28:30 +02:00
Andreas Rumpf
546a518b22 Merge branch 'devel' into araq-ic-fixes 2026-08-28 11:10:25 +02:00
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
Zoom
bd95f88f74 js: fix var openArray write-through for toOpenArray (#26086)
In the JS backend `toOpenArray` used `slice` (a copy), so writes through
a `var openArray` parameter silently vanished.

This emits `subarray` (a live shared-buffer view) for homogeneous
numeric arrays, otherwise such parameters are passed as a `{base, off,
len}` view that always aliases the caller's storage. Sliced seq/array
args become `{base, off, len}`, whole values `{base, off:0, len}`,
re-slices rebase.

Un-skips the JS guard in tests/openarray/topenarray.nim 
Fixes #15952.
2026-08-26 18:01:06 +02:00
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
araq
f3bdc6c5f2 IC: nim c --ic:on / nim cpp --ic:on replace the nim ic command
`nim ic` was a command of its own, which made it the C backend only and cut it
off from everything the ordinary compile commands accept. `--ic:on` is a switch
on `nim c` / `nim cpp` / `nim objc` instead, so `-r`, `-d:release`,
`--exceptions:`, and a project-wide opt-in from `nim.cfg` / `config.nims` all
work. `nim ic` still resolves to the same driver; `koch bootic`, `koch ic` and
`testament --ic` now go through the switch.

The switch was already parsed into a `conf.ic` nobody read. It is now read in
`passCmd1` as well, because `nim.nim` has to decide whether this run is an IC
DRIVER before config loading (`ensureIcConfig` produces the precompiled config
the driver itself replays); when the switch comes from a config file instead,
`main.nim` produces it late.

**C++.** `tests/cpp` passes under `--ic:on`, matching its classic result. Four
fixes, three of them the shape of every "C++ needs the whole program" problem the
per-module backend has:

* The driver DECLARES each module's translation unit to nifmake without loading a
  module, so it cannot ask `cgen.getCFile` — and it hardcoded `.nim.c`, so the
  merge stage went looking for `.c.nif` next to the `.cpp.nif` the children had
  written. `options.icCFileExt` mirrors the formula at backend granularity.

* C++ has no designated initializers, so the RTTI record is a bare variable that
  `DatInit` fills field by field. A bare `TNimTypeV2 x;` is a tentative
  definition — C's linker merges those, C++'s does not — so every TU that
  demanded the type defined it ("multiple definition of NTIv2__…"). It now gets
  the same extern-declaration + owned-`'d'`-definition split the C flavour has.

* `memberProcsPerType` / `initializersPerType` live only in the sem process, so
  the backend emitted a struct WITHOUT its in-class member declarations and the
  out-of-class definitions did not match ("no declaration matches
  'void Doo::memberProc()'"). They are replayed from a new `(repcppmember …)`
  log entry; `replayCppMember` re-derives the type from the routine's signature
  exactly as `semCppMember` does, so no type key has to survive the round trip.

* Two follow-ons for members: `loc.snippet` is a CALL PATTERN (`#->salute(@)`),
  and only `genMemberProcHeader` derives it — whole-program cgen got it for free
  by generating the defining module first, but the per-module backend emits that
  body in another process, leaving the caller with the mangled Nim name
  (`loo->salute_u0__vireouyks1()`). And that pattern is not a linker name: every
  `salute` member in every class mints the same one, so the merge stage handed
  them all to one artifact and dropped the rest (undefined vtable at link).
  Member definitions are keyed by their NIF name there instead.

`--run` is now dropped when re-invoking for the config artifact: the producer has
no output binary and `nim.nim`'s run step asserted on the empty `outFile`.

testament's `--ic` appends the switch rather than rewriting the compile verb, so
a test that overrides `cmd:` wholesale keeps its verb, and the C++ corpus is
covered too (it never was — the old rewrite only matched `nim c `).

`icFormatVersion` 36 -> 37 for the new log entry. `koch bootic` reaches its
byte-identical fixed point through the new entry point; `koch ic` passes;
`tests/ic`, `tests/destructor` (3 known `--newruntime` failures) and the classic
categories are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 10:32:31 +02:00
araq
7ddfc44c0f IC: six more correctness fixes found by the nim c oracle
`testament --ic` on `tests/destructor` went from 10 failures to 3; the three
left are all `--newruntime`, whose owned-ref RTTI destructor is still missing
(the object type's `=destroy` never reaches the `cg` that emits the type info).

* **global destructors are never run.** `graph.globalDestructors` is filled
  while a module's top level goes through `injectDestructorCalls`, and
  whole-program cgen empties the list into the main module's init proc — which
  IS the program body, so the calls land at exit. Under `nim ic` every module's
  `cg` is a separate process and main's only ever saw its own entries: a
  module-level `var` with a `=destroy` in an imported module was simply never
  destroyed. Each module now wraps its own list in a nullary exported proc and
  announces the name in its `.c.nif` meta head (`CnifVersion` 4 -> 5); main's
  `cg` reads the heads it already reads for init/datInit and calls them in
  reverse dependency order. New test `tests/ic/tglobal_dtors.nim` pins the
  order against the oracle.

* **`spawn` died with "system module needs: nimArgsPassingDone".** A module
  loaded from a NIF is named by its mangled suffix, so `loadCompilerProc`'s
  `module.name.s == "threadpool"` could never match. The backend loads the whole
  program before codegen, so consult every loaded module's index instead; misses
  are final and remembered (`getCompilerProc` doubles as a presence probe).

* **`new(x, finalizer)` died with "environment misses: x".** `semmagic`'s
  finalizer-to-destructor wrapper copies the original's AST but only rewrites
  the parameter, leaving `ast[namePos]` naming the ORIGINAL proc. ast2nif
  re-derives a routine's serialized AST from `ast[namePos].sym.ast`, so the
  wrapper serialized the original's body — whose parameter belongs to the
  original — and lambda lifting saw it as a captured variable of another proc.
  The copy now names itself, the invariant every other routine AST keeps.

* **a top-level `let (a, b) = f()` copied instead of moved.** A module's own
  symbol is the owner of every top-level symbol and is written as a real `(sd)`,
  so the loader minted a SECOND `skModule` PSym for it — and `sym.owner == owner`
  is an identity test in `aliasanalysis.isAnalysableFieldAccess`, which made
  every module-level location un-analysable. Hard error for a type with a
  disabled `=copy`. Bind the NIF name to the one registered module symbol.
  Backend only: doing it under `nim m` costs `times.toDateTimeByWeek` its
  inferred `raises`.

* **a doubly linked list leaked its whole contents.** A field USE serializes as
  a bare `SymUse` with nowhere to put symbol flags, so `trees.isCursor` said
  "not a cursor" for every loaded field and `DoublyLinkedNode.prev` became a
  COUNTED reference: every node held its predecessor alive and no refcount ever
  hit zero. `{.cursor.}` now rides in the NIF name marker (`` `fc `` next to
  `` `f ``), which def and use derive from the same `PSym`.

* **`--expandArc` came out shuffled.** `moduleSymbolStubs` iterated a `Table`,
  i.e. hash order, so the `lower` stage transformed a module's routines in an
  arbitrary order — not even stable between two compilers. Order by index
  offset, which is source order.

Also: `testament`'s `generatedFile` did not include the matrix entry in the
nimcache key its caller uses, so every `ccodeCheck` test with a `matrix:`
reported `reCodeNotFound`.

`koch bootic` still reaches its fixed point; `tests/ic` and the classic
`tests/destructor` are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 08:43:42 +02:00
ringabout
dc242e9027 fixes #26124; internal error: expr: param not init with nested generic procs (#26131)
fixes #26124

The fix preserves the resolved static value, allowing constant folding.

Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
2026-08-26 08:40:57 +02:00
araq
467c911dc5 IC testing: compare against nim c, and run the real corpus under nim ic
Every invariant `tests/ic` checked was IC-against-IC — clean == incremental, a
no-op edit changes nothing, a body edit moves no interface cookie. An IC that is
*consistently* wrong satisfies all of them, and that is exactly how two silent
miscompilations survived. `koch bootic` has the same blind spot: it proves the
compiler reproduces itself, not that it agrees with the reference backend.

Two mechanisms, at different scales.

**The oracle, in the metamorphic runner.** Every successful step is now also
compiled with `nim c` and run, and the two outputs must agree. Unlike the
hand-written `expect:` strings this needs no foresight from the test author: it
compares everything the program does, not only what someone thought to print,
which is what a silently-skipped destructor evades. `no-oracle` opts out.

The format also grew the expressiveness the recent bug hunt showed was missing —
every one of these described a state the suite could not reach:

* `#!DELETE <file>` removes a module. Deleting a still-imported file moves no
  mtime, so nothing re-fires.
* `#!FLAGS <switches>` changes the compiler switches between steps. A config
  change is not a file either.
* `fails: <substring>` asserts that BOTH compilers reject the program with that
  text. Previously every step had to succeed, so the whole error path — and
  recovery from it — was untested.

Six regression tests cover the eight bugs the last round fixed.
`testament r <file>` now dispatches metamorphic tests like `testament cat ic`.

**`testament --ic` runs the whole corpus through the incremental compiler**, so
IC inherits ~10k programs with expected output instead of 30 bespoke tests.
Two things had to change for that to mean anything:

* `nimcacheDir` now keys on the `matrix:` entry too. Two matrix variants of one
  file are two different compilations; sharing a cache meant each run
  invalidated what the previous left — harmless for a backend that caches only
  object files, useless for an incremental one.
* About half the corpus overrides the command wholesale (`cmd: "nim c --gc:arc
  $file"`), bypassing both `$target` and `$options`. Those are rewritten to `ic`
  and given a private cache.

**Warm cache, hastur-style.** A generated warmup program pulling in `system` and
the most-imported stdlib modules is compiled once per distinct compile
configuration into `nimcache/ic_warmup_<hash>`, and each test's empty cache is
seeded from it with mtimes preserved (nifmake compares output-mtime >
input-mtime, so stamping the copies "now" re-fires the whole graph). Only
program-independent artifacts are copied: the frontend NIFs and cookies plus the
per-module `lower`/`cg` output. The `.c`/`.o` are left behind on purpose — the
merge decision is whole-program, so they are re-rendered for every program
anyway.

`tests/destructor` (97 runs): `nim c` 35s cold / 32s warm; `nim ic` ~3m30 cold /
**9.8s warm**.

Compiler changes this required or uncovered:

* `merge` read the live-module list from a manifest the driver writes instead of
  globbing `*.c.nif` off the nimcache. Globbing absorbed artifacts belonging to
  any other program sharing the directory — which is what made a prefilled cache
  produce undefined symbols at link.
* The build-arg signature no longer includes `--icproject:`/`--icPreparsedConfig:`
  (they name where a build lives, not what it produces, so two caches holding
  identical artifacts got different signatures). The precompiled config still
  counts, by content hash, minus its `(nimcache …)` line.
* `.s.deps` seeding is speculative and runs before the prune, so a sidecar entry
  that has gone stale (an import that a `when` no longer takes) can be dropped
  instead of lingering forever; a pruned module's scan artifacts are deleted so
  an edit-accumulated cache still matches a clean one.
* A failed nifmake run no longer prints an `Error:` of its own. The children have
  already reported; adding a build-system status as the LAST error hid the
  compiler's real message from anything reading the final error — every
  reject-style test under `nim ic` said "nifmake failed with exit code: 1".
* `--mm:hooks` fed the mm mode to an on/off switch and failed outright with
  "'on' or 'off' expected, but 'hooks' found". Pre-existing and unrelated to IC;
  only reachable through the explicit switch, since `--newruntime` sets
  `selectedGC` directly.

Running `tests/destructor` under `--ic` currently leaves 10 failures. They are
genuine IC defects, not harness noise (all 97 pass under `nim c`) — the clearest
is `tglobaldestructor`: `graph.globalDestructors` is accumulated while injecting
destructors into a module's top level, but the main module's `cg` — which emits
the teardown — is a different process, so a module-level `var` with a `=destroy`
is never destroyed. Same shape as the init/datInit metas, and it wants the same
fix: record it in the `.c.nif` head.

Validation: `koch bootic` reaches its byte-identical fixed point; `tests/ic` is
36/36; arc, destructor, macros, template, iter, closure, ccg, codegen, types and
effects pass under `nim c`, with generics showing only its pre-existing failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 21:00:13 +02:00
araq
c01e58c146 IC: eight correctness fixes found by differential testing against nim c
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.

`icFormatVersion` 34 -> 35 for the two new NIF records.

Validation: `koch bootic` reaches its byte-identical fixed point; two clean-cache
builds from the same compiler are identical; testament `arc`, `destructor`,
`macros`, `template`, `iter`, `closure`, `ccg`, `codegen`, `types` and `effects`
pass, and `generics`, `ic` and `stdlib` show exactly the pre-existing failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 19:24:08 +02:00
Andreas Rumpf
8ca7b75b8b refactoring: better IC + no unique Id (#26137) 2026-08-25 11:59:19 +02:00
bptato
7f120229e8 Limit use of long checked integer ops to arm-none-eabi (#26121)
#23835 tried to do this, but it also switched to `long int` on the GNU
ABI, where it's actually just `int`.

Fixes #26111

Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
2026-08-24 10:16:32 +02:00
YesDrX
31215b3856 catch Defect in asynchttpserver for bad http request (#25820)
https://github.com/nim-lang/Nim/issues/25819

---------

Co-authored-by: Andreas Rumpf <araq4k@proton.me>
2026-08-23 17:06:44 +02:00
ringabout
2d1412a2ea fixes #26015; Multiple definition error when using codegenDecl regression (#26018)
fixes #26015

Fixes imported global variables with codegenDecl being emitted as
definitions instead of extern declarations.

A variable’s codegenDecl format should customize its definition in the
owning module. Other modules referencing the variable must emit a normal
declaration:

```c
extern NI variable;
```

After the variable-declaration builder refactor, genVarPrototype passed
Extern visibility to addVar. However, the sfCodegenDecl branch returned
before applying that visibility. This caused importing modules to emit
another tentative definition, resulting in duplicate-symbol linker
errors.

The fix restores the previous distinction between the custom definition
and cross-module prototypes. It also adds C and C++ regression coverage
for both direct access and access through an inline procedure.

follows up https://github.com/nim-lang/Nim/pull/24423
2026-08-23 12:37:15 +02:00
Constantine Molchanov
37223d2ea9 Feature: Rest: .. include::: Support :start-after: and :end-before: in :literal: mode (#26130)
With this addition, we can include code samples in the docs using
comments as achors. This is analogous to mdBook's
[shiftinclude](https://github.com/daviddrysdale/mdbook-shiftinclude)
preprocessor, which is used extensively in the Status projects docs,
e.g.:
https://github.com/status-im/nim-chronos/blob/master/docs/src/tutorials/http_client/chapter1.md?plain=1#L16

P.S. One missing piece would be the ability to de-dent the included code
automatically but that's a feature for another PR. This isn't as
critical as the ability to include parts of the code.
2026-08-23 12:36:25 +02:00
SirOlaf
6f1e6fdd06 Specialize rawAlloc for alignment (#26115)
Specialize `rawAlloc` for alignment (cherry-picked from the other PR).
This cuts the frame of the normal unaligned path down enough to regain
the performance lost from loading the cold page in #26110

Also cleans up `MemRegion` a bit, the regressions are either gone or
were measurement errors.
2026-08-23 07:36:10 +02:00
ringabout
f1256ddcf4 fixes #26123; Update PathKinds1 to include nkCast (#26126)
fixes #26123

`cast[T](x)` is a transparent path expression for compiler analysis.
Previously, move/alias analysis could fail to see a later use through a
cast and incorrectly mark the source as moved, causing the issue’s
segmentation fault.


for views,
https://nim-lang.org/docs/manual_experimental.html#view-types-path-expressions:
A cast expression cast[T](e) is a path expression.

It also affects skipConvDfa, isAnalysableFieldAccess, and aliases. And I
might narrow it down for the two cases above mentioned if it causes
problems
2026-08-21 21:57:51 +08:00
SirOlaf
901ca7905a IC: Do not serialize nfHasComment to nif (#26127)
It causes non-deterministic behavior because it's process-local.
2026-08-20 18:11:31 +02:00
Jake Leahy
81325d0745 Add checks to fromJson when trying to convert to an array (#26109)
Issue popped up when using `fromJson` into an array but the JSON passed
is an object

```nim
import std/[jsonutils, json]

let data = parseJson """
{"key": "value"}
"""
var foo: seq[int]
foo.fromJson(data)
echo foo #> @[0]
```
Basically the `setLen` would set the size to be equal to the number of
keys, but `getElems` just returns an empty array if the JSON isn't an
array which lead to it just creating zero'd items in the seq without
letting the user know.

Felt adding the checks was better than just skipping the `setLen` since
it lets the user know that there is a problem with the JSON
2026-08-19 08:24:25 +02:00
105 changed files with 8961 additions and 1883 deletions

View File

@@ -99,6 +99,7 @@ parameter and result types, not just their source-level shape. Use
works without single-quoting.
- `std/uri`: The `?` operator now appends query parameters to an existing query
string instead of replacing it. Fixes [#19782](https://github.com/nim-lang/Nim/issues/19782).
- `std/jsonutils`: `fromJson` now throws an exception when converting to `array`/`seq` if the JSON isn't an array instead of silently failing
## Language changes
@@ -149,6 +150,13 @@ parameter and result types, not just their source-level shape. Use
The issue was that `hasValuelessStatics` in `semtypinst.nim` didn't recognize
`tyTypeDesc(tyGenericParam)` as an unresolved generic parameter.
- The JS backend now implements write-through for `var openArray` parameters that
receive a `toOpenArray` view (bug #15952): mutations reach the caller's storage
instead of silently writing to a copy. Fixed homogeneous numeric arrays
(`array[N, T]`, JS typed arrays) slice via `subarray`; `seq` and non-numeric
arrays slice via a `{base, off, len}` view. This also covers seq/non-numeric-array
write-through, pass-through, re-slicing and `@` (openArray-to-seq) of such views.
## Tool changes
- Added `--raw` flag when generating JSON docs to not render markup.

View File

@@ -8,7 +8,7 @@ const
nkBracketExpr, nkDerefExpr, nkHiddenDeref,
nkAddr, nkHiddenAddr,
nkObjDownConv, nkObjUpConv}
PathKinds1* = {nkHiddenStdConv, nkHiddenSubConv}
PathKinds1* = {nkHiddenStdConv, nkHiddenSubConv, nkCast}
proc skipConvDfa*(n: PNode): PNode =
result = n
@@ -125,4 +125,3 @@ proc aliases*(obj, field: PNode): AliasKind =
else:
result = maybe
else: assert false # unreachable

View File

@@ -359,6 +359,18 @@ proc `flags=`*(t: PType, val: TTypeFlags) {.inline.} =
t.flagsImpl = val
proc sons*(t: PType): var TTypeSeq {.inline.} =
## The RAW child seq. Despite the name this is NOT the counterpart of the
## `sons` ITERATOR over a `PNode`, and it is not the way to walk a type's
## children — use `kids` / `ikids` / `paramTypes` / `signature`, or the named
## accessors (`returnType`, `baseClass`, `elementType`, `indexType`,
## `genericHead`, ...), which say WHICH child they mean.
##
## The difference is not cosmetic. A `tyProc` keeps its parameter types in
## `n`, not here — `setSons` asserts `sonsImpl.len <= 1` for one — so `[]`,
## `len` and every iterator built on them route parameters through
## `n[i].sym.typ`, while this seq holds only the return type. `for x in
## t.sons` therefore compiles, looks like the `PNode` idiom, and silently
## visits a different set of types.
if t.state == Partial: loadType(t)
result = t.sonsImpl
@@ -509,7 +521,8 @@ proc getPIdent*(a: PNode): PIdent {.inline.} =
of nkOpenSymChoice, nkClosedSymChoice, nkOpenSym: a.sons[0].sym.name
else: nil
template id*(a: PType | PSym): int = toId(a.itemId)
template id*(a: PSym): int = toId(a.itemId)
template id*(a: PType): int = toId(a.bindingId)
type
IdGenerator* = ref object # unfortunately, we really need the 'shared mutable' aspect here.
@@ -701,6 +714,10 @@ proc extractPragma*(s: PSym): PNode =
proc skipPragmaExpr*(n: PNode): PNode =
## if pragma expr, give the node the pragmas are applied to,
## otherwise give node itself
##
## `bnode` carries the `BNode` spelling. It is a separate one-liner rather
## than a shared template because this sits above the point in this module
## where `firstSon` for a `PNode` exists.
if n.kind == nkPragmaExpr:
result = n[0]
else:
@@ -764,10 +781,28 @@ when false:
echo k
echo v
when defined(icSymCount):
import std / [syncio, exitprocs, tables as symCountTables]
var symMints*: symCountTables.CountTable[string]
var symMintTotal*: int
var symCountHooked = false
proc newSym*(symKind: TSymKind, name: PIdent, idgen: IdGenerator; owner: PSym,
info: TLineInfo; options: TOptions = {}): PSym =
# generates a symbol and initializes the hash field too
assert not name.isNil
when defined(icSymCount):
# Counting symbol MINTS, not their names in the output: a gensym's number is
# its item id, so one extra symbol anywhere shifts every later name. A count
# is therefore far more sensitive than diffing generated C, and it localises
# the extra mint by kind instead of by whatever file happened to show it.
inc symMintTotal
symMints.inc $symKind
if not symCountHooked:
symCountHooked = true
addExitProc proc () =
stderr.writeLine "SYMMINT total=" & $symMintTotal
for k, v in symMints: stderr.writeLine "SYMMINT " & k & "=" & $v
let id = nextSymId idgen
result = PSym(name: name, kindImpl: symKind, flagsImpl: {}, infoImpl: info, itemId: id,
optionsImpl: options, ownerFieldImpl: owner, offsetImpl: defaultOffset,
@@ -1097,7 +1132,7 @@ proc newType*(kind: TTypeKind; idgen: IdGenerator; owner: PSym; son: sink PType
let id = nextTypeId idgen
result = PType(kind: kind, ownerFieldImpl: owner, sizeImpl: defaultSize,
alignImpl: defaultAlignment, itemId: id,
uniqueId: id, sonsImpl: @[])
bindingId: id, sonsImpl: @[])
if son != nil:
assert kind != tyProc
result.sonsImpl.add son
@@ -1173,18 +1208,23 @@ proc copyType*(t: PType, idgen: IdGenerator, owner: PSym): PType =
result.symImpl = t.sym # backend-info should not be copied
proc exactReplica*(t: PType; idgen: IdGenerator): PType =
## Replica that KEEPS `itemId` — the generic-param binding tables
## (`LayeredIdTable`) key on it, so the copy must keep matching its
## original — but mints a FRESH `uniqueId`: uniqueId is the SERIALIZATION
## identity (NIF type names key on it) and must be unique per instance.
## Replicas sharing the original's uniqueId serialized as duplicate defs
## under one NIF name; the loader collapsed them into a single type,
## Copy that INHERITS `bindingId` — the generic-param binding tables
## (`LayeredIdTable`) key on it, so the copy must keep matching its original
## there — while getting its own `itemId`, like every other type. The two
## remaining callers are `semtypinst.instCopyType` (a partially instantiated
## meta type must still bind in the next instantiation round) and the
## `tfUnresolved` typedesc replica in `semtypes.semTypeIdent`; everything
## else that used to come through here is a plain `copyType`.
##
## Do not "simplify" this to share `itemId` as well: `itemId` is the
## serialization identity, and replicas sharing it serialized as duplicate
## defs under one NIF name, which the loader collapsed into a single type —
## losing their flag differences (use-site `tfUnresolved` typedescs) or
## their structure (meta instance bodies shadowing a generic's canonical
## body).
result = PType(kind: t.kind, ownerFieldImpl: t.owner, sizeImpl: defaultSize,
alignImpl: defaultAlignment, itemId: t.itemId,
uniqueId: nextTypeId(idgen))
alignImpl: defaultAlignment, itemId: nextTypeId(idgen),
bindingId: t.bindingId)
assignType(result, t)
result.symImpl = t.sym # backend-info should not be copied
@@ -1446,18 +1486,28 @@ proc hasSubnodeWith*(n: PNode, kind: TNodeKind): bool =
return true
result = false
proc getInt*(a: PNode): Int128 =
case a.kind
of nkCharLit, nkUIntLit..nkUInt64Lit:
result = toInt128(cast[uint64](a.intVal))
of nkInt8Lit..nkInt64Lit:
result = toInt128(a.intVal)
of nkIntLit:
# XXX: enable this assert
# assert a.typ.kind notin {tyChar, tyUint..tyUInt64}
result = toInt128(a.intVal)
else:
raiseRecoverableError("cannot extract number from invalid AST node")
template getIntImpl*(aArg: typed): Int128 =
## The body of `getInt`, in a form `bnode.nim` can instantiate for a `BNode`
## too — same reason as `canRaiseImpl`: `BNode` is defined there and that
## module imports this one, so the shared logic has to live in a template
## rather than an `AnyNode` proc. There is no second copy.
block:
let a = aArg
var res: Int128
case a.kind
of nkCharLit, nkUIntLit..nkUInt64Lit:
res = toInt128(cast[uint64](a.intVal))
of nkInt8Lit..nkInt64Lit:
res = toInt128(a.intVal)
of nkIntLit:
# XXX: enable this assert
# assert a.typ.kind notin {tyChar, tyUint..tyUInt64}
res = toInt128(a.intVal)
else:
raiseRecoverableError("cannot extract number from invalid AST node")
res
proc getInt*(a: PNode): Int128 = getIntImpl(a)
proc getInt64*(a: PNode): int64 {.deprecated: "use getInt".} =
case a.kind
@@ -1477,14 +1527,21 @@ proc getFloat*(a: PNode): BiggestFloat =
#internalError(a.info, "getFloat")
#result = 0.0
proc getStr*(a: PNode): string =
case a.kind
of nkStrLit..nkTripleStrLit: result = a.strVal
of nkNilLit:
# let's hope this fixes more problems than it creates:
result = ""
else:
raiseRecoverableError("cannot extract string from invalid AST node")
template getStrImpl*(aArg: typed): string =
## Body shared with `bnode`'s `BNode` spelling — see `canRaiseImpl`.
block:
let gs = aArg
var res = ""
case gs.kind
of nkStrLit..nkTripleStrLit: res = gs.strVal
of nkNilLit:
# let's hope this fixes more problems than it creates:
res = ""
else:
raiseRecoverableError("cannot extract string from invalid AST node")
res
proc getStr*(a: PNode): string = getStrImpl(a)
#doAssert false, "getStr"
#internalError(a.info, "getStr")
#result = ""
@@ -1627,8 +1684,14 @@ proc isImportedException*(t: PType; conf: ConfigRef): bool =
let base = t.skipTypes({tyAlias, tyPtr, tyDistinct, tyGenericInst})
result = base.sym != nil and {sfCompileToCpp, sfImportc} * base.sym.flags != {}
proc isInfixAs*(n: PNode): bool =
return n.kind == nkInfix and n[0].kind == nkIdent and n[0].ident.id == ord(wAs)
template isInfixAsImpl*(nArg: typed): bool =
## Body shared with `bnode`'s `BNode` spelling — see `canRaiseImpl`.
block:
let ia = nArg
ia.kind == nkInfix and ia.firstSon.kind == nkIdent and
ia.firstSon.ident.id == ord(wAs)
proc isInfixAs*(n: PNode): bool = isInfixAsImpl(n)
proc skipColon*(n: PNode): PNode =
result = n
@@ -1705,36 +1768,110 @@ proc addParam*(procType: PType; param: PSym) =
procType.n.add newSymNode(param)
rawAddSon(procType, param.typ)
const magicsThatCanRaise = {
const magicsThatCanRaise* = {
mNone, mSlurp, mStaticExec, mParseExprToAst, mParseStmtToAst, mEcho}
proc canRaiseConservative*(fn: PNode): bool =
if fn.kind == nkSym and fn.sym.magic notin magicsThatCanRaise:
result = false
else:
result = true
# `canRaise` and `canRaiseConservative` are asked by the C backend, which is
# migrating to reading routine bodies straight off a `.bif` `Cursor` rather than
# off a materialised `PNode` tree (see `compiler/bnode.nim`). Both predicates
# only ever look at a node's `kind`, `sym` and `typ`, so ONE body serves either
# spelling -- but `BNode` is defined in `bnode.nim`, which imports this module,
# so the `BNode` overloads cannot live here. The bodies therefore live in
# templates and `bnode.nim` instantiates them for its own node type: one source
# of truth, no import cycle, and no second copy to keep in sync.
#
# The effect list is reached through `effectsOf` / `raisesNothing` rather than
# by subscripting `fn.typ.n`, so the templates below contain no knowledge of the
# layout and the `BNode` instantiation inherits none. `fn.typ` stays a `PType`
# in both spellings -- there is deliberately no `BType` (see `bnode.nim`) -- so
# what "works on a `.bif`" means for these two is that the type the decoder
# materialises must carry the same effect list the from-source one did. That is
# a claim about the WRITER, not about the vocabulary, and it is checked
# separately: `-d:icCanRaiseLog` logs every answer, and the same program built
# with and without `--ic:on` must produce the same verdicts.
proc canRaise*(fn: PNode): bool =
if fn.kind == nkSym and (fn.sym.magic notin magicsThatCanRaise or
{sfImportc, sfInfixCall} * fn.sym.flags == {sfImportc} or
sfGeneratedOp in fn.sym.flags):
result = false
elif fn.kind == nkSym and fn.sym.magic == mEcho:
result = true
elif fn.typ != nil and fn.typ.kind == tyProc and fn.typ.n != nil:
# TODO check for n having sons? or just return false for now if not
if fn.typ.n[0].kind == nkSym:
result = false
when defined(icCanRaiseLog):
var canRaiseBranch* = 0
## Which branch decided the last answer: 1 = the symbol's magic/flags,
## 2 = `mEcho`, 3 = the EFFECT LIST reached through `effectsOf`, 4 = the
## conservative predicate, 5 = short-circuited in `canRaiseDisp` before
## either predicate ran, 0 = fell through. Only branch 3 reads anything
## that had to survive a `.bif` round trip, so a differential in which no
## callee reaches it would prove nothing about the writer — which is the
## whole point of running the differential. See `-d:icCanRaiseLog`.
template markCanRaiseBranch*(n: int) =
when defined(icCanRaiseLog): canRaiseBranch = n
template canRaiseConservativeImpl*(fnArg: typed): bool =
block:
let fn = fnArg
markCanRaiseBranch 4
not (fn.kind == nkSym and fn.sym.magic notin magicsThatCanRaise)
proc effectsOf*(t: PType): PNode {.inline.} =
## The `nkEffectList` a proc type carries as child 0 of its formal-params
## node, with the parameters following from index 1 (`newProcType` builds it
## that way; `cgen` reads the params back with `sonsFrom(prc.typ.n, 1)`).
##
## Named rather than subscripted so that the layout is written down in ONE
## place. `.n` here is a TYPE's node, never a routine body, so it is always
## fully materialised and `firstSon` is safe — the `nfLazyBody` hazard that
## makes raw child access dangerous elsewhere (see `astdef.sons`) cannot reach
## it. A proc type always has this child; `t.n` with no children is not a
## shape the writer or sem produces, and this deliberately does not paper over
## one appearing.
result = if t.n == nil: nil else: t.n.firstSon
proc raisesNothing*(effects: PNode): bool =
## Whether an effect list says DEFINITIVELY that nothing is raised: it is long
## enough to have a raises slot at all, the slot is present, and it is empty.
##
## Every other shape — a list too short to carry the slot, an absent slot, a
## non-empty one — means the effects are unspecified or non-empty, and a
## caller must assume a raise. Stating it as the NEGATIVE is the point: the
## safe default has to be "can raise", so the one narrow case that licenses
## dropping an exception check is the one spelled out here, and a shape nobody
## anticipated falls on the conservative side by construction rather than by
## luck.
result = effects != nil and effects.len >= effectListLen and
effects[exceptionEffects] != nil and
effects[exceptionEffects].safeLen == 0
template canRaiseImpl*(fnArg: typed): bool =
block:
let fn = fnArg
var res: bool
if fn.kind == nkSym and (fn.sym.magic notin magicsThatCanRaise or
{sfImportc, sfInfixCall} * fn.sym.flags == {sfImportc} or
sfGeneratedOp in fn.sym.flags):
markCanRaiseBranch 1
res = false
elif fn.kind == nkSym and fn.sym.magic == mEcho:
markCanRaiseBranch 2
res = true
elif fn.typ != nil and fn.typ.kind == tyProc and fn.typ.n != nil:
markCanRaiseBranch 3
let effects = effectsOf(fn.typ)
if effects.kind == nkSym:
# The historical shape: slot 0 used to be an `nkType` before the effects
# moved in (see `newProcType`). Nothing to read, so nothing licenses a
# raise.
res = false
else:
# A proc-typed value with no explicit raises slot still has
# unspecified effects, which sempass2 treats conservatively.
# Codegen needs to do the same in order to keep goto-exception
# checks after indirect/closure calls.
res = not raisesNothing(effects)
else:
# A proc-typed value with no explicit raises slot still has
# unspecified effects, which sempass2 treats conservatively.
# Codegen needs to do the same in order to keep goto-exception
# checks after indirect/closure calls.
result = ((fn.typ.n[0].len < effectListLen) or
fn.typ.n[0][exceptionEffects] == nil or
fn.typ.n[0][exceptionEffects].safeLen > 0)
else:
result = false
markCanRaiseBranch 0
res = false
res
proc canRaiseConservative*(fn: PNode): bool = canRaiseConservativeImpl(fn)
proc canRaise*(fn: PNode): bool = canRaiseImpl(fn)
proc toHumanStrImpl[T](kind: T, num: static int): string =
result = $kind
@@ -1749,8 +1886,13 @@ proc toHumanStr*(kind: TTypeKind): string =
## strips leading `tk`
result = toHumanStrImpl(kind, 2)
proc skipHiddenAddr*(n: PNode): PNode {.inline.} =
(if n.kind == nkHiddenAddr: n[0] else: n)
template skipHiddenAddrImpl*(nArg: typed): untyped =
## Body shared with `bnode`'s `BNode` spelling — see `canRaiseImpl`.
block:
let sha = nArg
(if sha.kind == nkHiddenAddr: sha.firstSon else: sha)
proc skipHiddenAddr*(n: PNode): PNode {.inline.} = skipHiddenAddrImpl(n)
proc isNewStyleConcept*(n: PNode): bool {.inline.} =
assert n.kind == nkTypeClassTy

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@
import
ast, astyaml, options, lineinfos, idents, rodutils,
msgs
msgs, bnode
import std/[hashes, intsets]
import std/strutils except addf
@@ -100,7 +100,7 @@ proc skipConvCastAndClosure*(n: PNode): PNode =
result = result[1]
else: break
proc sameValue*(a, b: PNode): bool =
proc sameValue*[T: AnyNode](a, b: T): bool =
result = false
case a.kind
of nkCharLit..nkUInt64Lit:
@@ -639,9 +639,14 @@ proc getOrDefault*[T](t: TIdTable[T], key: ItemId): T =
if index >= 0: result = t.data[index].val
else: result = default(T)
template idTableGet*[T](t: TIdTable[T], key: PType | PSym): T =
template idTableGet*[T](t: TIdTable[T], key: PSym): T =
getOrDefault(t, key.itemId)
template idTableGet*[T](t: TIdTable[T], key: PType): T =
## Type-keyed tables are BINDING tables: an `exactReplica` must find what its
## original bound, hence `bindingId` and not the type's own identity.
getOrDefault(t, key.bindingId)
proc idTableRawInsert[T](data: var TIdPairSeq[T], key: ItemId, val: T) =
var h: Hash
let keyId = toId(key)
@@ -672,9 +677,12 @@ proc `[]=`*[T](t: var TIdTable[T], key: ItemId, val: T) =
idTableRawInsert(t.data, key, val)
inc(t.counter)
template idTablePut*[T](t: var TIdTable[T], key: PType | PSym, val: T) =
template idTablePut*[T](t: var TIdTable[T], key: PSym, val: T) =
t[key.itemId] = val
template idTablePut*[T](t: var TIdTable[T], key: PType, val: T) =
t[key.bindingId] = val
iterator idTablePairs*[T](t: TIdTable[T]): tuple[key: ItemId, val: T] =
for i in 0..high(t.data):
if not isNil(t.data[i].key):
@@ -732,7 +740,7 @@ proc listSymbolNames*(symbols: openArray[PSym]): string =
result.add ", "
result.add sym.name.s
proc isDiscriminantField*(n: PNode): bool =
if n.kind == nkCheckedFieldExpr: sfDiscriminant in n[0][1].sym.flags
elif n.kind == nkDotExpr: sfDiscriminant in n[1].sym.flags
proc isDiscriminantField*(n: AnyNode): bool =
if n.kind == nkCheckedFieldExpr: sfDiscriminant in n.firstSon.secondSon.sym.flags
elif n.kind == nkDotExpr: sfDiscriminant in n.secondSon.sym.flags
else: false

View File

@@ -784,11 +784,16 @@ type
# same id; there may be multiple copies of a type
# in memory!
# Keep in sync with PackedType
itemId*: ItemId
itemId*: ItemId # THE identity of this type: unique per instance, forever.
# Names the type in the NIF cache and decides which
# module owns its definition.
kind*: TTypeKind # kind of type
state*: ItemState
uniqueId*: ItemId # due to a design mistake, we need to keep the real ID here as it
# is required by the --incremental:on mode.
bindingId*: ItemId # the id of the type this one is a REPLICA of (its own
# `itemId` when it is not a replica). Only the generic
# binding tables (`LayeredIdTable` & friends) key on it:
# `exactReplica` produces a copy that must keep matching
# its original in those tables. Never an identity.
callConvImpl*: TCallingConvention # for procs
flagsImpl*: TTypeFlags # flags of the type
sonsImpl*: TTypeSeq # base types, etc.
@@ -952,13 +957,45 @@ iterator items*(n: PNode): PNode =
iterator sons*(n: PNode): PNode =
## Iterates over the children of `n`. Preferred over `for i in 0..<n.len: n[i]`
## as it does not rely on random indexed access (see doc/ic_backend_nif_native.md).
## as it does not rely on random indexed access, and over `for x in n.sons`,
## which reads the raw FIELD and so skips the `len` hook that materialises a
## deferred `nfLazyBody` body — over such a body that loop silently visits
## nothing. See `compiler/bnode.nim` for the backend vocabulary this feeds.
for i in 0..<n.safeLen: yield n[i]
iterator isons*(n: PNode): tuple[i: int, n: PNode] =
## Like `sons` but also yields the child index. Replaces
## `for i in 0..<n.len: ... n[i] ...` when `i` itself is still needed.
for i in 0..<n.safeLen: yield (i, n[i])
iterator isons*(n: PNode; start = 0): tuple[i: int, n: PNode] =
## Like `sons` but also yields the child index, and optionally skips the first
## `start` children. Replaces `for i in start..<n.len: ... n[i] ...` when `i`
## itself is still needed — for a parameter position, a `needTmp[i-1]` lookup,
## a parallel index into the routine's `PType`, and so on. `start` is almost
## always 1, to step over a call's callee or a case statement's selector.
##
## Use `sonsFrom` instead when the index is only ever used to subscript `n`.
for i in start..<n.safeLen: yield (i, n[i])
iterator sonsFrom*(n: PNode; start: int): PNode =
## `sons` skipping the first `start` children. Replaces
## `for i in start..<n.len: ... n[i] ...`, which is by far the commonest
## indexed shape in the code generator — `start` is almost always 1, to step
## over a case/try statement's selector or a call's callee.
for i in start..<n.safeLen: yield n[i]
iterator sonsButLast*(n: PNode; count = 1): PNode =
## `sons` without the last `count` children. Replaces `for i in 0..<n.len-1:
## ... n[i] ...`, which is what an `nkOfBranch`/`nkExceptBranch` walk looks
## like: the last child is the branch BODY, the ones before it are the labels
## it matches. `count = 2` is the `nkVarTuple`/`nkIdentDefs` shape, whose last
## two children are the type and the value. A `Cursor` can serve this with a
## single pass and `count` nodes of lookahead; the indexed form has to re-walk
## the children for every label.
##
## Use `isonsButLast` instead when the index is still needed.
for i in 0 ..< n.safeLen - count: yield n[i]
iterator isonsButLast*(n: PNode; count = 1): tuple[i: int, n: PNode] =
## Like `sonsButLast` but also yields the child index — for a tuple field
## position, a parallel index into the tuple's `PType`, and so on.
for i in 0 ..< n.safeLen - count: yield (i, n[i])
when defined(useNodeIds):
const nodeIdToDebug* = -1 # 2322968
@@ -1041,10 +1078,56 @@ proc newStrNode*(strVal: string; info: TLineInfo): PNode =
# handling for IC, they end up in IC indexes etc. Thus we "log" them in the module graph
# and to pass them around to the NIF writer. This is not very elegant but it works.
const
InstanceDisambBit* = 0x4000_0000'i32
## Set in the `disamb` of routine instances whose value is content-derived
## (see `modulegraphs.setInstanceDisamb`); keeps them disjoint from the
## small counter range ordinary symbols draw from, so the NIF name
## `name.disamb.module` stays collision-free within a module.
HookDisambBit* = 0x2000_0000'i32
## Set in the `disamb` of synthesized type-bound operators and `$enum`
## procs whose value is content-derived (see `modulegraphs.setHookDisamb`);
## disjoint from both the small counter range and `InstanceDisambBit`.
##
## Both live here rather than in `modulegraphs` because `ast2nif` — which
## cannot import that module — names symbols by them.
proc backendMintedDisamb*(s: PSym): int32 {.inline.} =
## The integer that identifies a BACKEND-MINTED symbol (`isBackendMinted`) in
## every name derived from it: its NIF name (`ast2nif.toNifSymName`) and its C
## name (`mangleutils.mangleProcNameExt`, `ccgutils.makeUnique`).
##
## Two cases, and the whole point of having ONE function is that all three
## sites take the same one:
##
## * A lifted HOOK's `disamb` is CONTENT-derived (`modulegraphs.setHookDisamb`),
## so it is identical in every process. Such a hook really does cross process
## boundaries — `lower` mints the env hooks of nested routines while `cg`
## mints those of the module's top level, and both land in the same
## translation unit — and its C name is also baked into emit-everywhere RTTI
## tables. `itemId.item` would differ per process, so two unrelated hooks
## collided on one `_c<item>` and the merge stage kept a single body for both
## (C accepted the mistyped call, C++ rejected it).
## * Otherwise `itemId.item` — the writer's dedup identity, unique per `@bk`
## sym. `disamb` cannot serve here: a module's `:env` syms are minted from TWO
## id spaces (the backend `lower` stage's idgen and sem's `vmTransfIdgen`)
## whose `disambTable`s each start `:env` at the same low count, so a
## macro-lowered and a backend-lowered `:env` collide on `:env.2.<mod>@bk`.
##
## The loader copies the name's numeric component back into `disamb`, so after a
## round trip `disamb` equals this value and `ast2nif.globalName` — which always
## reads `disamb` — agrees with the name the writer produced.
##
## This rule used to be written out at each of the three sites. They drifted:
## `toNifSymName` lacked the hook exception, so a content-derived value was
## overwritten by the loader and two backend hooks merged into one C function.
if (s.disamb and HookDisambBit) != 0'i32: s.disamb
else: s.itemId.item
type
LogEntryKind* = enum
HookEntry, ConverterEntry, MethodEntry, EnumToStrEntry, GenericInstEntry,
PureEnumEntry
PureEnumEntry, CppMemberEntry
LogEntry* = object
kind*: LogEntryKind
op*: TTypeAttachedOp
@@ -1091,7 +1174,7 @@ proc forcePartial*(s: PSym) =
proc forcePartial*(t: PType) =
## Resets all impl-fields to their default values and sets state to Partial.
## This is useful for creating a stub type that can be lazily loaded later.
## The fields itemId, kind, uniqueId are preserved.
## The fields itemId, kind, bindingId are preserved.
t.state = Partial
t.callConvImpl = ccNimCall
t.flagsImpl = {}

1132
compiler/bnode.nim Normal file

File diff suppressed because it is too large Load Diff

335
compiler/bodynav.nim Normal file
View File

@@ -0,0 +1,335 @@
#
#
# The Nim Compiler
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## `BodyNav` — a scope-chained navigator over a `.bif` routine body.
##
## Ported from Nimony's `nimony/typenav.nim` (`TypeCache` / `TypeScope`). The
## idea being stolen is not the type algebra — we do not need it, `typ` returns
## a fully materialized `PType` — but the SHAPE of the resolution context:
##
## * a chain of scope frames, each a small table, linked to its parent;
## * `openScope` / `closeScope` / `registerLocal`, called BY THE TRAVERSAL as it
## descends and as it walks past each definition;
## * a lookup that consults the chain and, on a miss, falls through to the
## module index (`typenav`'s `tryLoadSym`; here the decoder's own
## `symFromCursor`).
##
## The consequence is the point: the scope is a PRODUCT OF THE WALK. Nothing is
## snapshotted, so nothing can be stale, and a reader that starts at the top of
## a body and descends always has exactly the definitions it has already passed.
##
## WHAT THIS REPLACES. `ast2nif.PendingBody` stashes `localSyms` — a COPY of the
## enclosing sym def's local symbols, taken when the body was deferred — and
## `bnode`'s `BodyScope` then copies it again. `materializeLazyBody` loads the
## body with its own `var pb`, so every definition the load creates lands in a
## table that is discarded on return. A cursor-side reader holding the earlier
## copy therefore cannot see them, and would mint its own `PSym` for the same
## name: two objects, one symbol.
##
## HOW BIG THAT PROBLEM ACTUALLY IS, measured rather than assumed. Build with
## `-d:icLocalSymStats` and every process reports its `localSyms` traffic on
## exit. Over a full `--ic:on` build of the standard-library closure (104
## backend processes):
##
## localHit=0 fieldStub=2 miss=0 sdReg=5902 extractReg=45
##
## Definitions register constantly and NOT ONE use ever resolves through the
## table. The reason is `ast2nif.isLocalSym`, which returns a hardwired `false`:
## every symbol is emitted with a module suffix and resolves through the
## decoder's global `syms` memo, so both spellings get the same `PSym` whatever
## either one has cached. The 5902 registrations are object FIELDS, whose uses
## deliberately go to `loadFieldStub` instead.
##
## So the stale snapshot is a LATENT hazard, not a live bug, and this module is
## not a bug fix — it is the mechanism that keeps it latent once `isLocalSym`
## stops being `false`, or once a body-local name appears for any other reason.
## Said plainly so nobody has to re-derive it: today the nav changes no answers,
## and the grinder in `cgen` proves that by requiring the navigated symbol to be
## the same object the `PNode` loader produced, at every node of every body.
##
## It is not decorative either, and that also has a number. Over the same build,
## the grinder's traversal reports `navHits=42236 navFallbacks=12658
## navRegistered=311`: the chain answers 77% of lookups, and 311 definitions are
## registered by the walk rather than read from a table someone filled in
## earlier. Sabotaging the key (truncating it to three characters, so
## `c_fwrite` and `c_fflush` collide) makes the grinder fail on the first body
## it reaches — so a clean run means the resolution is right, not that the
## lookup never happened.
##
## FIELDS ARE NOT REGISTERED, and that is deliberate. `loadFieldStub` mints a
## fresh stub per use because two distinct fields can share a name (and a
## position) across types — `a.x` and `b.x` in one body are two different
## symbols. Caching a field by its bare name would hand the second use the first
## one's stub, and its type. The nav skips field names entirely and leaves that
## path exactly as it was.
import std / tables
import ast, ast2nif
when defined(nimPreviewSlimSystem):
import std / assertions
import "../dist/nimony/src/lib/nifcore" except pool
type
NavScopeKind* = enum
nsBlock, ## an ordinary nested scope
nsRoutine ## a routine boundary — see `crossedRoutines`
NavScope {.acyclic.} = ref object
locals: Table[string, PSym]
parent: NavScope
kind: NavScopeKind
BridgeTables* = ref object
## The side tables of an IN-PROCESS bridged buffer (`nodebridge.nim`).
## A `.bif` names its symbols because the reader is a different process; a
## buffer built and read inside ONE process does not have to, and paying the
## name round trip anyway would be worse than pointless — it is what makes
## the file path unable to give a field a stable identity (`loadFieldStub`
## mints per use). Here a symbol reference is an index and resolution hands
## back the very same object, so `symAt` is exact and idempotent for every
## symbol kind, fields included.
syms*: seq[PSym]
types*: seq[PType]
origins*: Table[int, PNode]
## Token position -> the `PNode` encoded there, so a cursor can name the
## node it came from. Lives here rather than in `BridgeBuf` because the
## lookup has to be reachable from wherever a location is built, which is
## everywhere in the generator — the same reason `syms` is here.
buf*: ptr TokenBuf
## The buffer `origins` is keyed against; `cursorToPosition` needs it.
## Borrowed, not owned: it points into the `BridgeBuf` that a scoped
## `withBridge` is currently reading, and never outlives it.
BodyNav* = object
## The resolution context for ONE routine body. `base` is what the decoder
## itself needs (the owning module plus a table `loadSymStub` can write
## into); the frame chain on top of it is this module's contribution.
##
## `bridge` is non-nil only while reading a bridged buffer. It is consulted
## FIRST and, when it answers, it answers exactly — there is no fallback,
## because a `(bsym …)` index that the tables cannot resolve is a corrupt
## buffer, not a cache miss.
base*: BodyScope
bridge*: BridgeTables
current: NavScope
hits*: int ## resolved from the chain
fallbacks*: int ## resolved through the decoder
registered*: int ## definitions the walk registered
proc originAt*(t: BridgeTables; c: Cursor): PNode =
## The source node a cursor was encoded from, or nil when there is none (a
## `DotToken`, or a cursor that is not at a node head).
if t == nil or t.buf == nil: return nil
result = t.origins.getOrDefault(cursorToPosition(t.buf[], c), nil)
proc initBodyNav*(base: sink BodyScope): BodyNav =
## A nav over a body, seeded with whatever resolution context the decoder
## handed out. The root frame is a routine frame: a body IS one.
result = BodyNav(base: base,
current: NavScope(locals: initTable[string, PSym](),
parent: nil, kind: nsRoutine))
proc initBridgeNav*(tables: BridgeTables): BodyNav =
## A nav over an in-process bridged buffer. `base` stays empty — a bridged
## buffer names nothing, so there is nothing for the decoder to resolve — but
## the ROOT FRAME still has to exist: a walk brackets its descent with
## `openScope`/`closeScope`, and a nav without a root frame makes the first
## `closeScope` pop past the bottom.
result = BodyNav(bridge: tables,
current: NavScope(locals: initTable[string, PSym](),
parent: nil, kind: nsRoutine))
proc openScope*(nav: var BodyNav; kind = nsBlock) {.inline.} =
nav.current = NavScope(locals: initTable[string, PSym](),
parent: nav.current, kind: kind)
proc closeScope*(nav: var BodyNav) {.inline.} =
doAssert nav.current.parent != nil, "closeScope past the root frame"
nav.current = nav.current.parent
template withScope*(nav: var BodyNav; kind: NavScopeKind; body: untyped) =
openScope(nav, kind)
try:
body
finally:
closeScope(nav)
proc registerLocal*(nav: var BodyNav; name: string; s: PSym) {.inline.} =
## Record a definition the walk has just passed, in the innermost frame.
nav.current.locals[name] = s
inc nav.registered
proc lookupLocal*(nav: BodyNav; name: string): PSym =
## The chain only. `nil` when nothing in scope carries this name.
var it {.cursor.} = nav.current
while it != nil:
let s = it.locals.getOrDefault(name)
if s != nil: return s
it = it.parent
result = nil
proc crossedRoutines*(nav: BodyNav; name: string): int =
## How many routine frames separate the use from the definition — 0 when the
## definition is in the current routine. `typenav` computes the same thing as
## `LocalInfo.crossedProc`, and it is what tells a closure pass that a name is
## captured rather than local. Nothing consumes it here yet; it is the reason
## the frames carry a kind at all, and dropping the kind would make it
## unrecoverable later.
var it {.cursor.} = nav.current
var crossed = 0
while it != nil:
if it.locals.getOrDefault(name) != nil: return crossed
if it.kind == nsRoutine: inc crossed
it = it.parent
result = -1
# ---------------------------------------------------------------------------
# Names
#
# A symbol reaches the reader in four shapes and they all NAME the same thing;
# `navName` is the one place that knows which token holds the name, so the
# lookup key is derived identically no matter which wrapper the writer chose.
proc navName*(n: Cursor): string =
## The NIF name a token denotes, or `""` when the token names no symbol.
case nifcore.kind(n)
of Symbol, SymbolDef:
result = symName(n)
of TagLit:
let tag = n.tags.tagName(cursorTagId(n))
if tag == symDefTagName:
let name = childCursor(n)
result = if nifcore.kind(name) in {Symbol, SymbolDef}: symName(name) else: ""
elif tag == hiddenTypeTagName:
# `(ht <type> <sym>)`
var inner = childCursor(n)
skip inner
result = navName(inner)
elif tag == symNodeFlagsTagName:
# `(nflags <flags> <symnode>)`
var inner = childCursor(n)
skip inner
result = navName(inner)
else:
result = ""
else:
result = ""
proc symToken*(n: Cursor): Cursor =
## The token that actually NAMES the symbol, with the wrappers stripped.
## `loadSymStub` accepts a `Symbol`, a `SymbolDef` or an `(sd ...)` and
## rejects everything else, so the `(ht ...)` / `(nflags ...)` forms have to be
## peeled here rather than at each call site — the same peeling `navName` does
## for the key, kept beside it so the two cannot drift apart.
result = n
while nifcore.kind(result) == TagLit:
let tag = result.tags.tagName(cursorTagId(result))
if tag == hiddenTypeTagName or tag == symNodeFlagsTagName:
var inner = childCursor(result)
skip inner # the explicit type / the node flags
result = inner
else:
break
proc cacheFrame(nav: var BodyNav): NavScope =
## Where a decoder-resolved name is remembered: the nearest ROUTINE frame.
## Not the innermost frame — a `.bif` name is unique within its module (see
## `isLocalSym`), so its meaning cannot change between frames, and caching it
## deeper would only throw it away sooner. Not the root either, so that a
## nested routine's names die with the nested routine.
result = nav.current
while result.kind != nsRoutine and result.parent != nil:
result = result.parent
proc bridgeIndex(n: Cursor; tag: string): int =
## The `<intlit>` payload of a `(bsym …)` / `(btyp …)` token, or -1 when `n`
## is not that shape.
result = -1
if nifcore.kind(n) == TagLit and n.tags.tagName(cursorTagId(n)) == tag:
let payload = childCursor(n)
if nifcore.kind(payload) == IntLit:
result = int(nifcore.intVal(payload))
proc symAt*(nav: var BodyNav; n: Cursor): PSym =
## The symbol a token names: the bridge first (exact), then the chain, then
## the decoder.
if nav.bridge != nil:
let idx = bridgeIndex(symToken(n), bridgeSymTagName)
if idx >= 0:
doAssert idx < nav.bridge.syms.len,
"bridged sym index out of range: " & $idx
inc nav.hits
return nav.bridge.syms[idx]
let name = navName(n)
if name.len > 0:
let cached = lookupLocal(nav, name)
if cached != nil:
inc nav.hits
return cached
inc nav.fallbacks
result = symFromCursor(program, symToken(n), nav.base)
if result != nil and name.len > 0 and not isFieldNifName(name):
cacheFrame(nav).locals[name] = result
proc typeAt*(nav: var BodyNav; n: Cursor): PType =
## Types are not navigated: `ast2nif` already materializes them lazily from
## the module's type index, keyed by name, so there is no per-body state to
## keep and nothing a frame could cache that the decoder does not already.
if nav.bridge != nil:
if nifcore.kind(n) == DotToken: return nil
let idx = bridgeIndex(n, bridgeTypeTagName)
if idx >= 0:
doAssert idx < nav.bridge.types.len,
"bridged type index out of range: " & $idx
return nav.bridge.types[idx]
result = typeFromCursor(program, n, nav.base)
# ---------------------------------------------------------------------------
# Registration during a walk
proc registerDefHere*(nav: var BodyNav; n: Cursor): bool {.discardable.} =
## Register `n` if `n` ITSELF is a definition; do not descend. This is the
## incremental half: a walk calls it on each child before recursing into it,
## so a use can only resolve from the chain to a definition the walk has
## already passed. A use that precedes its definition simply misses and falls
## through to the decoder, which is the behaviour there was before — the nav
## degrades to the old path rather than answering wrongly.
result = false
if nifcore.kind(n) == TagLit and
n.tags.tagName(cursorTagId(n)) == symDefTagName:
let name = navName(n)
if name.len > 0 and not isFieldNifName(name):
let s = symFromCursor(program, n, nav.base)
if s != nil:
registerLocal(nav, name, s)
result = true
proc registerDefs*(nav: var BodyNav; n: Cursor) =
## Register every definition in the SUBTREE at `n` — `typenav.registerLocals`
## with the recursion left in, because a Nim body puts `nkIdentDefs` under an
## `nkVarSection` under the statement list rather than declaring at one level.
##
## Call it on entering a scope to get the eager behaviour (every definition
## known before any use is resolved, which is what a RANDOM-ACCESS reader
## needs), or per statement to get the incremental one (only definitions
## already walked past are visible, which is what a real pass wants and what
## makes use-before-def detectable rather than silently working).
if nifcore.kind(n) == TagLit and
n.tags.tagName(cursorTagId(n)) == symDefTagName:
let name = navName(n)
if name.len > 0 and not isFieldNifName(name):
let s = symFromCursor(program, n, nav.base)
if s != nil: registerLocal(nav, name, s) # `(sd ...)` needs no peeling
return
var c = childCursor(n)
while c.hasMore:
registerDefs(nav, c)
skip c

View File

@@ -326,7 +326,7 @@ proc startStruct(obj: var Builder; m: BModule; t: PType; name: string; baseType:
# rest of the options add a field or don't need it due to inheritance,
# we need to add the dummy field for uncheckedarray ahead of time
# so that it remains trailing
if t.itemId notin m.g.graph.memberProcsPerType and
if t.bindingId notin m.g.graph.memberProcsPerType and
t.n != nil and t.n.len == 1 and t.n[0].kind == nkSym and
t.n[0].sym.typ.skipTypes(abstractInst).kind == tyUncheckedArray:
# only consists of flexible array field, add *initial* dummy field
@@ -341,7 +341,7 @@ proc startStruct(obj: var Builder; m: BModule; t: PType; name: string; baseType:
proc finishStruct(obj: var Builder; m: BModule; t: PType; info: StructBuilderInfo) =
if info.baseKind == bcNone and info.preFieldsLen == obj.buf.len and
t.itemId notin m.g.graph.memberProcsPerType:
t.bindingId notin m.g.graph.memberProcsPerType:
# no fields were added, add dummy field
obj.addField(name = "dummy", typ = CChar)
if info.named:

View File

@@ -9,9 +9,19 @@
#
# included from cgen.nim
proc canRaiseDisp(p: BProc; n: PNode): bool =
proc canRaiseDisp(p: BProc; n: AnyNode): bool =
# we assume things like sysFatal cannot raise themselves
if n.kind == nkSym and {sfNeverRaises, sfImportc, sfCompilerProc} * n.sym.flags != {}:
# 5 = "decided here, neither predicate ran". Without resetting, the marker
# keeps whatever the PREVIOUS call left in it and the early return below
# attributes this answer to a branch that did not execute — which is how the
# first run of this differential came to claim effect-list coverage it did
# not have. Both short-circuits below leave it at 5.
markCanRaiseBranch 5
if n.kind == nkSym and n.sym.kind == skMethod:
# A base method may be overridden by a branch with a wider exception set.
# Its inferred effects describe only the base body, not every vtable target.
result = true
elif n.kind == nkSym and {sfNeverRaises, sfImportc, sfCompilerProc} * n.sym.flags != {}:
result = false
elif optPanics in p.config.globalOptions or
(n.kind == nkSym and sfSystemModule in getModule(n.sym).flags and
@@ -21,8 +31,22 @@ proc canRaiseDisp(p: BProc; n: PNode): bool =
else:
# we have to be *very* conservative:
result = canRaiseConservative(n)
when defined(icCanRaiseLog):
# `canRaise` reads the raises spec off `fn.typ.n`, and under `--ic:on` that
# node came back from a `.bif`. Whether it came back INTACT is not something
# the `BNode`/`PNode` grinder can answer — both spellings ask the same
# `PType` and so agree however wrong it is. The only oracle is the same
# program built without IC. Log the verdict per callee; the two builds must
# produce the same one.
if n.kind == nkSym:
logCanRaise(n.sym, result)
proc preventNrvo(p: BProc; dest, le, ri: PNode): bool =
proc preventNrvo(p: BProc; dest, le: PNode; ri: AnyNode): bool =
## `dest` and `le` stay `PNode`s: they are DESTINATIONS, which the whole call
## family keeps as `PNode`s so they can be nil and so they can be handed to
## the alias analysis, and it is also what keeps the `warnObservableStores`
## message able to RENDER `le` — rendering being a capability the cursor seam
## does not have at all. `ri`, the call being generated, is a cursor.
proc locationEscapes(p: BProc; le: PNode; inTryStmt: bool): bool =
result = false
var n = le
@@ -42,16 +66,17 @@ proc preventNrvo(p: BProc; dest, le, ri: PNode): bool =
nkCheckedFieldExpr:
n = n.firstSon
of nkHiddenStdConv, nkHiddenSubConv, nkConv:
n = n[1]
n = n.secondSon
else:
# cannot analyse the location; assume the worst
return true
result = false
if le != nil:
for i in 1..<ri.len:
let r = ri[i]
if isPartOf(le, r, {pfStructural}) != arNo: return true
for r in sonsFrom(ri, 1):
# `isPartOf` compares field symbols by identity and so has not moved to
# the seam; `origin` hands it the same nodes it always compared.
if isPartOf(le, origin(r), {pfStructural}) != arNo: return true
# we use the weaker 'canRaise' here in order to prevent too many
# annoying warnings, see #14514
if canRaise(ri.firstSon) and
@@ -59,11 +84,10 @@ proc preventNrvo(p: BProc; dest, le, ri: PNode): bool =
message(p.config, le.info, warnObservableStores, $le)
# bug #19613 prevent dangerous aliasing too:
if dest != nil and dest != le:
for i in 1..<ri.len:
let r = ri[i]
if isPartOf(dest, r, {pfStructural}) != arNo: return true
for r in sonsFrom(ri, 1):
if isPartOf(dest, origin(r), {pfStructural}) != arNo: return true
proc hasNoInit(call: PNode): bool {.inline.} =
proc hasNoInit(call: AnyNode): bool {.inline.} =
result = call.firstSon.kind == nkSym and sfNoInit in call.firstSon.sym.flags
proc isHarmlessStore(p: BProc; canRaise: bool; d: TLoc): bool =
@@ -95,7 +119,11 @@ proc cleanupTemp(p: BProc; returnType: PType, tmp: TLoc): bool =
else:
result = false
proc fixupCall(p: BProc, le, ri: PNode, d: var TLoc,
# `le` — the assignment DESTINATION — stays a `PNode` throughout this family.
# It is nilable (`genCall` passes nil, and a cursor has no standalone nil), and
# it is what `preventNrvo` and `isPartOf` are handed, both of which are still
# `PNode`-typed. `ri`, the expression being generated, is the part that moves.
proc fixupCall(p: BProc, le: PNode, ri: AnyNode, d: var TLoc,
result: var Builder, call: var CallBuilder) =
let canRaise = p.config.exc == excGoto and canRaiseDisp(p, ri.firstSon)
genLineDir(p, ri)
@@ -179,14 +207,14 @@ proc fixupCall(p: BProc, le, ri: PNode, d: var TLoc,
proc genBoundsCheck(p: BProc; arr, a, b: TLoc; arrTyp: PType)
proc reifiedOpenArray(n: PNode): bool {.inline.} =
proc reifiedOpenArray(n: AnyNode): bool {.inline.} =
var x = n
while true:
case x.kind
of {nkAddr, nkHiddenAddr, nkHiddenDeref}:
x = x.firstSon
of nkHiddenStdConv:
x = x[1]
x = x.secondSon
else:
break
if x.kind == nkSym and x.sym.kind == skParam:
@@ -194,10 +222,10 @@ proc reifiedOpenArray(n: PNode): bool {.inline.} =
else:
result = true
proc genOpenArraySlice(p: BProc; q: PNode; formalType, destType: PType; prepareForMutation = false): (Rope, Rope) =
var a = initLocExpr(p, q[1])
var b = initLocExpr(p, q[2])
var c = initLocExpr(p, q[3])
proc genOpenArraySlice(p: BProc; q: AnyNode; formalType, destType: PType; prepareForMutation = false): (Rope, Rope) =
var a = initLocExpr(p, q.secondSon)
var b = initLocExpr(p, son(q, 2))
var c = initLocExpr(p, son(q, 3))
# bug #23321: In the function mapType, ptrs (tyPtr, tyVar, tyLent, tyRef)
# are mapped into ctPtrToArray, the dereference of which is skipped
# in the `genDeref`. We need to skip these ptrs here
@@ -223,7 +251,7 @@ proc genOpenArraySlice(p: BProc; q: PNode; formalType, destType: PType; prepareF
let lit = cIntLiteral(first)
result = (cCast(ptrType(dest), cOp(Add, NimInt, ra, cOp(Sub, NimInt, rb, lit))), lengthExpr)
of tyOpenArray, tyVarargs:
let data = if reifiedOpenArray(q[1]): dotField(ra, "Field0") else: ra
let data = if reifiedOpenArray(q.secondSon): dotField(ra, "Field0") else: ra
result = (cCast(ptrType(dest), cOp(Add, NimInt, data, rb)), lengthExpr)
of tyUncheckedArray, tyCstring:
result = (cCast(ptrType(dest), cOp(Add, NimInt, ra, rb)), lengthExpr)
@@ -257,26 +285,26 @@ proc genOpenArraySlice(p: BProc; q: PNode; formalType, destType: PType; prepareF
result = ("", "")
internalError(p.config, "openArrayLoc: " & typeToString(a.t))
proc openArrayLoc(p: BProc, formalType: PType, n: PNode; result: var Builder) =
proc openArrayLoc(p: BProc, formalType: PType, n: AnyNode; result: var Builder) =
var q = skipConv(n)
var skipped = false
while q.kind == nkStmtListExpr and q.len > 0:
while q.kind == nkStmtListExpr and q.hasSons:
skipped = true
q = q.lastSon
if getMagic(q) == mSlice:
# magic: pass slice to openArray:
if skipped:
q = skipConv(n)
while q.kind == nkStmtListExpr and q.len > 0:
for i in 0..<q.len-1:
genStmts(p, q[i])
while q.kind == nkStmtListExpr and q.hasSons:
for it in sonsButLast(q):
genStmts(p, it)
q = q.lastSon
let (x, y) = genOpenArraySlice(p, q, formalType, n.typ.elementType)
result.add(x)
result.addArgumentSeparator()
result.add(y)
else:
var a = initLocExpr(p, if n.kind == nkHiddenStdConv: n[1] else: n)
var a = initLocExpr(p, if n.kind == nkHiddenStdConv: n.secondSon else: n)
case skipTypes(a.t, abstractVar+{tyStatic}).kind
of tyOpenArray, tyVarargs:
let ra = rdLoc(a)
@@ -367,13 +395,13 @@ proc expressionsNeedsTmp(p: BProc, a: TLoc): TLoc =
result = getTemp(p, a.lode.typ, needsInit=false)
genAssignment(p, result, a, {})
proc genArgStringToCString(p: BProc, n: PNode; result: var Builder; needsTmp: bool) {.inline.} =
proc genArgStringToCString(p: BProc, n: AnyNode; result: var Builder; needsTmp: bool) {.inline.} =
var a = initLocExpr(p, n.firstSon)
let tmp = withTmpIfNeeded(p, a, needsTmp)
let ra = if p.config.usesSso(): byRefLoc(p, tmp) else: tmp.rdLoc
result.addCall(cgsymValue(p.module, "nimToCStringConv"), ra)
proc genArg(p: BProc, n: PNode, param: PSym; call: PNode; result: var Builder; needsTmp = false) =
proc genArg(p: BProc, n: AnyNode, param: PSym; call: AnyNode; result: var Builder; needsTmp = false) =
var a: TLoc
if n.kind == nkStringToCString:
genArgStringToCString(p, n, result, needsTmp)
@@ -393,10 +421,16 @@ proc genArg(p: BProc, n: PNode, param: PSym; call: PNode; result: var Builder; n
# will be a reference in C++ and we cannot create a temporary reference
# variable. Thus, we create a temporary pointer variable instead.
let needsIndirect = mapType(p.config, n.firstSon.typ, mapTypeChooser(n.firstSon) == skParam) != ctArray
# A REWRITE, and one that has to be followed. The node's type is replaced in
# place, and a cursor would keep reading the type slot as it was ENCODED —
# the buffer does not see the mutation. So from here this site works on the
# origin, which is the node being mutated and therefore the one that has the
# new type.
let nn = origin(n)
if needsIndirect:
n.typ = n.typ.exactReplica(p.module.idgen)
n.typ.incl tfVarIsPtr
a = initLocExprSingleUse(p, n)
nn.typ = copyType(nn.typ, p.module.idgen, nn.typ.owner)
nn.typ.incl tfVarIsPtr
a = initLocExprSingleUse(p, nn)
a = withTmpIfNeeded(p, a, needsTmp)
if needsIndirect: a.flags.incl lfIndirect
# if the proc is 'importc'ed but not 'importcpp'ed then 'var T' still
@@ -418,7 +452,7 @@ proc genArg(p: BProc, n: PNode, param: PSym; call: PNode; result: var Builder; n
addRdLoc(withTmpIfNeeded(p, a, needsTmp), result)
#assert result != nil
proc genArgNoParam(p: BProc, n: PNode; result: var Builder; needsTmp = false) =
proc genArgNoParam(p: BProc, n: AnyNode; result: var Builder; needsTmp = false) =
var a: TLoc
if n.kind == nkStringToCString:
genArgStringToCString(p, n, result, needsTmp)
@@ -428,65 +462,81 @@ proc genArgNoParam(p: BProc, n: PNode; result: var Builder; needsTmp = false) =
import aliasanalysis
proc potentialAlias(n: PNode, potentialWrites: seq[PNode]): bool =
proc potentialAlias(n: AnyNode, potentialWrites: seq[PNode]): bool =
result = false
for p in potentialWrites:
if p.aliases(n) != no or n.aliases(p) != no:
return true
proc skipTrivialIndirections(n: PNode): PNode =
proc skipTrivialIndirections[T: AnyNode](n: T): T =
## Explicitly generic rather than `(n: AnyNode): AnyNode`: two occurrences of
## a type class in one signature are two INDEPENDENT parameters, so that
## spelling would let the result type drift from the argument's.
result = n
while true:
case result.kind
of nkDerefExpr, nkHiddenDeref, nkAddr, nkHiddenAddr, nkObjDownConv, nkObjUpConv:
result = result.firstSon
of nkHiddenStdConv, nkHiddenSubConv:
result = result[1]
result = result.secondSon
else: break
proc getPotentialReads(n: PNode; result: var seq[PNode]) =
proc getPotentialReads(n: AnyNode; result: var seq[PNode]) =
case n.kind:
of nkLiterals, nkIdent, nkFormalParams: discard
of nkSym: result.add n
else:
for s in n:
for s in sons(n):
getPotentialReads(s, result)
proc genParams(p: BProc, ri: PNode, typ: PType; result: var Builder, argBuilder: var CallBuilder) =
proc genParams(p: BProc, ri: AnyNode, typ: PType; result: var Builder, argBuilder: var CallBuilder) =
# We must generate temporaries in cases like #14396
# to keep the strict Left-To-Right evaluation
var needTmp = newSeq[bool](ri.len - 1)
# The arguments are walked BACKWARDS below, which a `Cursor` cannot do and
# which costs a re-walk per step even on a `PNode`. Materialize them in one
# forward pass and index that; `needTmp` already allocates per call, so this
# is the same order of work.
#
# The arguments are materialized as `PNode`s, not cursors, because the alias
# analysis below (`potentialAlias`, `getPotentialReads`) carries a
# `seq[PNode]` beside the node and has not moved to the seam — see the
# mixed-representation blocker in `bnode`'s module doc. `origin` gives the
# same objects the tree-driven build used, so this is the argument list it
# always was; when that analysis moves, this becomes `seq[AnyNode]`.
var args: seq[PNode] = @[]
for it in sonsFrom(ri, 1): args.add origin(it)
var needTmp = newSeq[bool](args.len)
var potentialWrites: seq[PNode] = @[]
for i in countdown(ri.len - 1, 1):
if ri[i].skipTrivialIndirections.kind == nkSym:
needTmp[i - 1] = potentialAlias(ri[i], potentialWrites)
for i in countdown(args.high, 0):
if args[i].skipTrivialIndirections.kind == nkSym:
needTmp[i] = potentialAlias(args[i], potentialWrites)
else:
#if not ri[i].typ.isCompileTimeOnly:
#if not args[i].typ.isCompileTimeOnly:
var potentialReads: seq[PNode] = @[]
getPotentialReads(ri[i], potentialReads)
getPotentialReads(args[i], potentialReads)
for n in potentialReads:
if not needTmp[i - 1]:
needTmp[i - 1] = potentialAlias(n, potentialWrites)
getPotentialWrites(ri[i], false, potentialWrites)
if not needTmp[i]:
needTmp[i] = potentialAlias(n, potentialWrites)
getPotentialWrites(args[i], false, potentialWrites)
when false:
# this optimization is wrong, see bug #23748
if ri[i].kind in {nkHiddenAddr, nkAddr}:
if args[i].kind in {nkHiddenAddr, nkAddr}:
# Optimization: don't use a temp, if we would only take the address anyway
needTmp[i - 1] = false
needTmp[i] = false
for i in 1..<ri.len:
for i, it in isons(ri, 1):
if i < typ.n.len:
assert(typ.n[i].kind == nkSym)
let paramType = typ.n[i]
assert(son(typ.n, i).kind == nkSym)
let paramType = son(typ.n, i)
if not paramType.typ.isCompileTimeOnly:
var arg = newBuilder("")
genArg(p, ri[i], paramType.sym, ri, arg, needTmp[i-1])
genArg(p, it, paramType.sym, ri, arg, needTmp[i-1])
if arg.buf.len != 0:
result.addArgument(argBuilder):
result.add(extract(arg))
else:
var arg = newBuilder("")
genArgNoParam(p, ri[i], arg, needTmp[i-1])
genArgNoParam(p, it, arg, needTmp[i-1])
if arg.buf.len != 0:
result.addArgument(argBuilder):
result.add(extract(arg))
@@ -496,7 +546,7 @@ proc addActualSuffixForHCR(res: var Rope, module: PSym, sym: PSym) =
(sym.typ.callConv == ccInline or sym.owner.id == module.id):
res = res & "_actual".rope
proc genPrefixCall(p: BProc, le, ri: PNode, d: var TLoc) =
proc genPrefixCall(p: BProc, le: PNode, ri: AnyNode, d: var TLoc) =
# this is a hotspot in the compiler
var op = initLocExpr(p, ri.firstSon)
# getUniqueType() is too expensive here:
@@ -512,7 +562,7 @@ proc genPrefixCall(p: BProc, le, ri: PNode, d: var TLoc) =
genParams(p, ri, typ, res, call)
fixupCall(p, le, ri, d, res, call)
proc genClosureCall(p: BProc, le, ri: PNode, d: var TLoc) =
proc genClosureCall(p: BProc, le: PNode, ri: AnyNode, d: var TLoc) =
template callProc(rp, params, pTyp: Snippet): Snippet =
let e = dotField(rp, "ClE_0")
@@ -547,6 +597,12 @@ proc genClosureCall(p: BProc, le, ri: PNode, d: var TLoc) =
var argBuilder = default(CallBuilder) # not initCallBuilder, we just want the params
genParams(p, ri, typ, params, argBuilder)
# `rawProc` is bound BEFORE the `{.dirty.}` template that uses it. Inside a
# generic proc a dirty template's identifiers resolve at instantiation, and a
# local declared after the template loses to the module-level `rawProc` proc
# — which type-checks as a completely different thing.
let rawProc = getClosureType(p.module, typ, clHalf)
template genCallPattern {.dirty.} =
let rp = rdLoc(op)
let pars = extract(params)
@@ -555,8 +611,6 @@ proc genClosureCall(p: BProc, le, ri: PNode, d: var TLoc) =
p.s(cpsStmts).add(callIter(rp, pars))
else:
p.s(cpsStmts).add(callProc(rp, pars, rawProc))
let rawProc = getClosureType(p.module, typ, clHalf)
let canRaise = p.config.exc == excGoto and canRaiseDisp(p, ri.firstSon)
if typ.returnType != nil:
if isInvalidReturnType(p.config, typ):
@@ -608,27 +662,27 @@ proc genClosureCall(p: BProc, le, ri: PNode, d: var TLoc) =
genCallPattern()
if canRaise: raiseExit(p)
proc genOtherArg(p: BProc; ri: PNode; i: int; typ: PType; result: var Builder;
proc genOtherArg(p: BProc; ri: AnyNode; i: int; typ: PType; result: var Builder;
argBuilder: var CallBuilder) =
if i < typ.n.len:
# 'var T' is 'T&' in C++. This means we ignore the request of
# any nkHiddenAddr when it's a 'var T'.
let paramType = typ.n[i]
let paramType = son(typ.n, i)
assert(paramType.kind == nkSym)
if paramType.typ.isCompileTimeOnly:
discard
elif paramType.typ.kind in {tyVar} and ri[i].kind == nkHiddenAddr:
elif paramType.typ.kind in {tyVar} and son(ri, i).kind == nkHiddenAddr:
result.addArgument(argBuilder):
genArgNoParam(p, ri[i].firstSon, result)
genArgNoParam(p, son(ri, i).firstSon, result)
else:
result.addArgument(argBuilder):
genArgNoParam(p, ri[i], result) #, typ.n[i].sym)
genArgNoParam(p, son(ri, i), result) #, son(typ.n, i).sym)
else:
if tfVarargs notin typ.flags:
localError(p.config, ri.info, "wrong argument count")
else:
result.addArgument(argBuilder):
genArgNoParam(p, ri[i], result)
genArgNoParam(p, son(ri, i), result)
discard """
Dot call syntax in C++
@@ -667,7 +721,7 @@ y.v() --> y.v() is correct
"""
proc skipAddrDeref(node: PNode): PNode =
proc skipAddrDeref[T: AnyNode](node: T): T =
var n = node
var isAddr = false
case n.kind
@@ -685,15 +739,15 @@ proc skipAddrDeref(node: PNode): PNode =
else:
result = node
proc genThisArg(p: BProc; ri: PNode; i: int; typ: PType; result: var Builder) =
proc genThisArg(p: BProc; ri: AnyNode; i: int; typ: PType; result: var Builder) =
# for better or worse c2nim translates the 'this' argument to a 'var T'.
# However manual wrappers may also use 'ptr T'. In any case we support both
# for convenience.
internalAssert p.config, i < typ.n.len
assert(typ.n[i].kind == nkSym)
assert(son(typ.n, i).kind == nkSym)
# if the parameter is lying (tyVar) and thus we required an additional deref,
# skip the deref:
var ri = ri[i]
var ri = son(ri, i)
while ri.kind == nkObjDownConv: ri = ri.firstSon
let t = typ[i].skipTypes({tyGenericInst, tyAlias, tySink})
if t.kind in {tyVar}:
@@ -717,22 +771,22 @@ proc genThisArg(p: BProc; ri: PNode; i: int; typ: PType; result: var Builder) =
else:
ri = skipAddrDeref(ri)
if ri.kind in {nkAddr, nkHiddenAddr}: ri = ri.firstSon
genArgNoParam(p, ri, result) #, typ.n[i].sym)
genArgNoParam(p, ri, result) #, son(typ.n, i).sym)
result.add(".")
proc genPatternCall(p: BProc; ri: PNode; pat: string; typ: PType; result: var Builder) =
proc genPatternCall(p: BProc; ri: AnyNode; pat: string; typ: PType; result: var Builder) =
var i = 0
var j = 1
while i < pat.len:
case pat[i]
of '@':
var callBuilder = default(CallBuilder) # not init call builder
for k in j..<ri.len:
for k, _ in isons(ri, j):
genOtherArg(p, ri, k, typ, result, callBuilder)
inc i
of '#':
if i+1 < pat.len and pat[i+1] in {'+', '@'}:
let ri = ri[j]
let ri = son(ri, j)
if ri.kind in nkCallKinds:
let typ = skipTypes(ri.firstSon.typ, abstractInst)
if pat[i+1] == '+': genArgNoParam(p, ri.firstSon, result)
@@ -740,7 +794,7 @@ proc genPatternCall(p: BProc; ri: PNode; pat: string; typ: PType; result: var Bu
if 1 < ri.len:
var callBuilder: CallBuilder = default(CallBuilder)
genOtherArg(p, ri, 1, typ, result, callBuilder)
for k in j+1..<ri.len:
for k, _ in isons(ri, j+1):
var callBuilder: CallBuilder = default(CallBuilder)
genOtherArg(p, ri, k, typ, result, callBuilder)
result.add(")")
@@ -751,7 +805,7 @@ proc genPatternCall(p: BProc; ri: PNode; pat: string; typ: PType; result: var Bu
genThisArg(p, ri, j, typ, result)
inc i
elif i+1 < pat.len and pat[i+1] == '[':
var arg = ri[j].skipAddrDeref
var arg = son(ri, j).skipAddrDeref
while arg.kind in {nkAddr, nkHiddenAddr, nkObjDownConv}: arg = arg.firstSon
genArgNoParam(p, arg, result)
#result.add debugTree(arg, 0, 10)
@@ -774,7 +828,7 @@ proc genPatternCall(p: BProc; ri: PNode; pat: string; typ: PType; result: var Bu
if i - 1 >= start:
result.add(substr(pat, start, i - 1))
proc genInfixCall(p: BProc, le, ri: PNode, d: var TLoc) =
proc genInfixCall(p: BProc, le: PNode, ri: AnyNode, d: var TLoc) =
var op = initLocExpr(p, ri.firstSon)
# getUniqueType() is too expensive here:
var typ = skipTypes(ri.firstSon.typ, abstractInst)
@@ -811,11 +865,11 @@ proc genInfixCall(p: BProc, le, ri: PNode, d: var TLoc) =
pl.add(op.snippet)
var res = newBuilder("")
var call = initCallBuilder(res, extract(pl))
for i in 2..<ri.len:
for i, _ in isons(ri, 2):
genOtherArg(p, ri, i, typ, res, call)
fixupCall(p, le, ri, d, res, call)
proc genNamedParamCall(p: BProc, ri: PNode, d: var TLoc) =
proc genNamedParamCall(p: BProc, ri: AnyNode, d: var TLoc) =
# generates a crappy ObjC call
var op = initLocExpr(p, ri.firstSon)
var pl = newBuilder("[")
@@ -832,25 +886,25 @@ proc genNamedParamCall(p: BProc, ri: PNode, d: var TLoc) =
pl.add(op.snippet)
if ri.len > 1:
pl.add(": ")
genArg(p, ri[1], typ.n[1].sym, ri, pl)
genArg(p, ri.secondSon, typ.n.secondSon.sym, ri, pl)
start = 2
else:
if ri.len > 1:
genArg(p, ri[1], typ.n[1].sym, ri, pl)
genArg(p, ri.secondSon, typ.n.secondSon.sym, ri, pl)
pl.add(" ")
pl.add(op.snippet)
if ri.len > 2:
pl.add(": ")
genArg(p, ri[2], typ.n[2].sym, ri, pl)
for i in start..<ri.len:
genArg(p, son(ri, 2), son(typ.n, 2).sym, ri, pl)
for i, it in isons(ri, start):
if i >= typ.n.len:
internalError(p.config, ri.info, "varargs for objective C method?")
assert(typ.n[i].kind == nkSym)
var param = typ.n[i].sym
assert(son(typ.n, i).kind == nkSym)
var param = son(typ.n, i).sym
pl.add(" ")
pl.add(param.name.s)
pl.add(": ")
genArg(p, ri[i], param, ri, pl)
genArg(p, it, param, ri, pl)
if typ.returnType != nil:
if isInvalidReturnType(p.config, typ):
if ri.len > 1: pl.add(" ")
@@ -882,11 +936,11 @@ proc genNamedParamCall(p: BProc, ri: PNode, d: var TLoc) =
p.s(cpsStmts).addStmt():
p.s(cpsStmts).add(extract(pl))
proc notYetAlive(n: PNode): bool {.inline.} =
proc notYetAlive(n: AnyNode): bool {.inline.} =
let r = getRoot(n)
result = r != nil and r.loc.lode == nil
proc isInactiveDestructorCall(p: BProc, e: PNode): bool =
proc isInactiveDestructorCall(p: BProc, e: AnyNode): bool =
#[ Consider this example.
var :tmpD_3281815
@@ -903,10 +957,10 @@ proc isInactiveDestructorCall(p: BProc, e: PNode): bool =
We want to return early but the 'finally' section is traversed before
the 'let args = ...' statement. We exploit this to generate better
code for 'return'. ]#
result = e.len == 2 and e.firstSon.kind == nkSym and
e.firstSon.sym.name.s == "=destroy" and notYetAlive(e[1].skipAddr)
result = e.safeLen == 2 and e.firstSon.kind == nkSym and
e.firstSon.sym.name.s == "=destroy" and notYetAlive(e.secondSon.skipAddr)
proc genAsgnCall(p: BProc, le, ri: PNode, d: var TLoc) =
proc genAsgnCall(p: BProc, le: PNode, ri: AnyNode, d: var TLoc) =
if p.withinBlockLeaveActions > 0 and isInactiveDestructorCall(p, ri):
return
when defined(icDbgHash):
@@ -928,4 +982,4 @@ proc genAsgnCall(p: BProc, le, ri: PNode, d: var TLoc) =
else:
genPrefixCall(p, le, ri, d)
proc genCall(p: BProc, e: PNode, d: var TLoc) = genAsgnCall(p, nil, e, d)
proc genCall(p: BProc, e: AnyNode, d: var TLoc) = genAsgnCall(p, nil, e, d)

File diff suppressed because it is too large Load Diff

View File

@@ -53,11 +53,11 @@ proc genStringLiteralDataOnlyV1(m: BModule, s: string; result: var Rope) =
res.add(makeCString(s))
m.s[cfsStrData].add(extract(res))
proc genStringLiteralV1(m: BModule; n: PNode; result: var Builder) =
proc genStringLiteralV1(m: BModule; n: AnyNode; result: var Builder) =
if s.isNil:
result.add(cCast(ptrType(cgsymValue(m, "NimStringDesc")), NimNil))
else:
let id = nodeTableTestOrSet(m.dataCache, n, m.labels)
let id = nodeTableTestOrSet(m.dataCache, origin(n), m.labels)
var name: string = ""
if id == m.labels:
# string literal not found in the cache:
@@ -85,8 +85,8 @@ proc genStringLiteralDataOnlyV2(m: BModule, s: string; result: Rope; isConst: bo
res.add(makeCString(s))
m.s[cfsStrData].add(extract(res))
proc genStringLiteralV2(m: BModule; n: PNode; isConst: bool; result: var Builder) =
let id = nodeTableTestOrSet(m.dataCache, n, m.labels)
proc genStringLiteralV2(m: BModule; n: AnyNode; isConst: bool; result: var Builder) =
let id = nodeTableTestOrSet(m.dataCache, origin(n), m.labels)
var litName: string
if id == m.labels:
cgsym(m, "NimStrPayload")
@@ -111,8 +111,8 @@ proc genStringLiteralV2(m: BModule; n: PNode; isConst: bool; result: var Builder
res.add(cCast(ptrType("NimStrPayload"), cAddr(litName)))
m.s[cfsStrData].add(extract(res))
proc genStringLiteralV2Const(m: BModule; n: PNode; isConst: bool; result: var Builder) =
let id = nodeTableTestOrSet(m.dataCache, n, m.labels)
proc genStringLiteralV2Const(m: BModule; n: AnyNode; isConst: bool; result: var Builder) =
let id = nodeTableTestOrSet(m.dataCache, origin(n), m.labels)
var pureLit: Rope
if id == m.labels:
pureLit = getTempName(m)
@@ -164,7 +164,7 @@ proc ssoMoreLit(m: BModule; s: string): string =
val = val or (ch shl (uint(ptrSize - 1 - i) * 8))
result = cCast(ptrType("LongString"), "(uintptr_t)" & $val)
proc genStringLiteralV3Const(m: BModule; n: PNode; isConst: bool; result: var Builder) =
proc genStringLiteralV3Const(m: BModule; n: AnyNode; isConst: bool; result: var Builder) =
# Inline SmallString struct initializer for use inside const aggregate types.
# Layout: {bytes: NimUint, more: ptr LongString}
# bytes = slen (low byte) | char[0]<<8 | char[1]<<16 | ... | char[6]<<56
@@ -220,7 +220,7 @@ proc genStringLiteralV3Const(m: BModule; n: PNode; isConst: bool; result: var Bu
# ------ Version 3: SmallString (SSO) strings --------------------------------
proc genStringLiteralV3(m: BModule; n: PNode; isConst: bool; result: var Builder) =
proc genStringLiteralV3(m: BModule; n: AnyNode; isConst: bool; result: var Builder) =
# SmallString literal. Always generate a fresh SmallString variable (like v2
# always generates a fresh outer NimStringV2). For long strings, cache the
# LongString payload to avoid duplicates within a module.
@@ -259,7 +259,7 @@ proc genStringLiteralV3(m: BModule; n: PNode; isConst: bool; result: var Builder
else:
# Long: cache the LongString block to emit it only once per module per string.
# Always generate a fresh SmallString pointing at the (possibly cached) block.
let id = nodeTableTestOrSet(m.dataCache, n, m.labels)
let id = nodeTableTestOrSet(m.dataCache, origin(n), m.labels)
var dataName: string
if id == m.labels:
dataName = getTempName(m)
@@ -301,7 +301,7 @@ proc genStringLiteralV3(m: BModule; n: PNode; isConst: bool; result: var Builder
proc genNilStringLiteral(m: BModule; info: TLineInfo; result: var Builder) =
result.add(cCast(ptrType(cgsymValue(m, "NimStringDesc")), NimNil))
proc genStringLiteral(m: BModule; n: PNode; result: var Builder) =
proc genStringLiteral(m: BModule; n: AnyNode; result: var Builder) =
case detectStrVersion(m)
of 0, 1: genStringLiteralV1(m, n, result)
of 2: genStringLiteralV2(m, n, isConst = true, result)

View File

@@ -19,18 +19,17 @@ proc specializeResetN(p: BProc, accessor: Rope, n: PNode;
if n == nil: return
case n.kind
of nkRecList:
for i in 0..<n.len:
specializeResetN(p, accessor, n[i], typ)
for it in sons(n):
specializeResetN(p, accessor, it, typ)
of nkRecCase:
if (n[0].kind != nkSym): internalError(p.config, n.info, "specializeResetN")
let disc = n[0].sym
if (n.firstSon.kind != nkSym): internalError(p.config, n.info, "specializeResetN")
let disc = n.firstSon.sym
if disc.loc.snippet == "": fillObjectFields(p.module, typ)
if disc.loc.t == nil:
internalError(p.config, n.info, "specializeResetN()")
let discField = dotField(accessor, disc.loc.snippet)
p.s(cpsStmts).addSwitchStmt(discField):
for i in 1..<n.len:
let branch = n[i]
for branch in sonsFrom(n, 1):
assert branch.kind in {nkOfBranch, nkElse}
var caseBuilder: SwitchCaseBuilder
p.s(cpsStmts).addSwitchCase(caseBuilder):

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ type
proc genTraverseProc(c: TTraversalClosure, accessor: Rope, typ: PType)
proc genCaseRange(p: BProc, branch: PNode, info: var SwitchCaseBuilder)
proc genCaseRange(p: BProc, branch: AnyNode, info: var SwitchCaseBuilder)
proc getTemp(p: BProc, t: PType, needsInit=false): TLoc
proc visit(p: BProc, data, visitor: Snippet) =
@@ -31,19 +31,18 @@ proc genTraverseProc(c: TTraversalClosure, accessor: Rope, n: PNode;
if n == nil: return
case n.kind
of nkRecList:
for i in 0..<n.len:
genTraverseProc(c, accessor, n[i], typ)
for it in sons(n):
genTraverseProc(c, accessor, it, typ)
of nkRecCase:
if (n[0].kind != nkSym): internalError(c.p.config, n.info, "genTraverseProc")
if (n.firstSon.kind != nkSym): internalError(c.p.config, n.info, "genTraverseProc")
var p = c.p
let disc = n[0].sym
let disc = n.firstSon.sym
if disc.loc.snippet == "": fillObjectFields(c.p.module, typ)
if disc.loc.t == nil:
internalError(c.p.config, n.info, "genTraverseProc()")
let discField = dotField(accessor, disc.loc.snippet)
p.s(cpsStmts).addSwitchStmt(discField):
for i in 1..<n.len:
let branch = n[i]
for branch in sonsFrom(n, 1):
assert branch.kind in {nkOfBranch, nkElse}
var caseBuilder: SwitchCaseBuilder
p.s(cpsStmts).addSwitchCase(caseBuilder):

View File

@@ -59,10 +59,10 @@ proc mangleProc(m: BModule; s: PSym; makeUnique: bool): string =
result = "_Z" # Common prefix in Itanium ABI
var params = ""
var staticLists = ""
if s.typ.len > 1: #we dont care about the return param
for i in 1..<s.typ.len:
if s.typ[i].isNil: continue
params.add encodeType(m, s.typ[i], staticLists)
if s.typ.paramsLen > 0: # we dont care about the return param
for _, pt in paramTypes(s.typ):
if pt.isNil: continue
params.add encodeType(m, pt, staticLists)
result.add encodeSym(m, s, makeUnique, staticLists)
result.add params
@@ -311,7 +311,7 @@ proc isInvalidReturnType(conf: ConfigRef; typ: PType, isProc = true): bool =
var rettype = typ
var isAllowedCall = true
if isProc:
rettype = rettype[0]
rettype = rettype.returnType
isAllowedCall = typ.callConv in {ccClosure, ccInline, ccNimCall}
if rettype == nil or (isAllowedCall and
getSize(conf, rettype) > conf.target.floatSize*3):
@@ -480,7 +480,7 @@ proc getTypeDescWeak(m: BModule; t: PType; check: var IntSet; kind: TypeDescKind
of tySequence:
let sig = hashType(t, m.config)
if optSeqDestructors in m.config.globalOptions:
if skipTypes(etB[0], typedescInst).kind == tyEmpty:
if skipTypes(etB.elementType, typedescInst).kind == tyEmpty:
internalError(m.config, "cannot map the empty seq type to a C type")
result = cacheGetType(m.forwTypeCache, sig)
@@ -524,7 +524,7 @@ proc seqV2ContentType(m: BModule; t: PType; check: var IntSet) =
if result == "":
discard getTypeDescAux(m, t, check, dkVar)
else:
let dataTyp = getTypeDescAux(m, t.skipTypes(abstractInst)[0], check, dkVar)
let dataTyp = getTypeDescAux(m, t.skipTypes(abstractInst).elementType, check, dkVar)
m.s[cfsTypes].addSimpleStruct(m, name = result & "_Content", baseType = ""):
m.s[cfsTypes].addField(name = "cap", typ = NimInt)
m.s[cfsTypes].addField(name = "data",
@@ -598,10 +598,10 @@ proc genMemberProcParams(m: BModule; prc: PSym, superCall, rettype, name, params
rettype = runtimeFormat(rettype.replace("'0", "$1"), [getTypeDescAux(m, t.returnType, check, dkResult)])
var types, names, args: seq[string] = @[]
if not isCtor:
var this = t.n[1].sym
var this = t.n.secondSon.sym
backendEnsureMutable this
fillParamName(m, this)
fillLoc(this.locImpl, locParam, t.n[1],
fillLoc(this.locImpl, locParam, t.n.secondSon,
this.paramStorageLoc)
if this.typ.kind == tyPtr:
this.locImpl.snippet = "this"
@@ -611,9 +611,9 @@ proc genMemberProcParams(m: BModule; prc: PSym, superCall, rettype, name, params
types.add getTypeDescWeak(m, this.typ, check, dkParam)
let firstParam = if isCtor: 1 else: 2
for i in firstParam..<t.n.len:
if t.n[i].kind != nkSym: internalError(m.config, t.n.info, "genMemberProcParams")
var param = t.n[i].sym
for it in sonsFrom(t.n, firstParam):
if it.kind != nkSym: internalError(m.config, t.n.info, "genMemberProcParams")
var param = it.sym
var descKind = dkParam
if optByRef in param.options:
if param.typ.kind == tyGenericInst:
@@ -623,7 +623,7 @@ proc genMemberProcParams(m: BModule; prc: PSym, superCall, rettype, name, params
var typ, name: string
backendEnsureMutable param
fillParamName(m, param)
fillLoc(param.locImpl, locParam, t.n[i],
fillLoc(param.locImpl, locParam, it,
param.paramStorageLoc)
if ccgIntroducedPtr(m.config, param, t.returnType) and descKind == dkParam:
typ = getTypeDescWeak(m, param.typ, check, descKind) & "*"
@@ -668,9 +668,9 @@ proc genProcParams(m: BModule; t: PType, rettype: var Rope, params: var Builder,
rettype = getTypeDescWeak(m, t.returnType, check, dkResult)
var paramBuilder: ProcParamBuilder
params.addProcParams(paramBuilder):
for i in 1..<t.n.len:
if t.n[i].kind != nkSym: internalError(m.config, t.n.info, "genProcParams")
var param = t.n[i].sym
for child in sonsFrom(t.n, 1):
if child.kind != nkSym: internalError(m.config, t.n.info, "genProcParams")
var param = child.sym
# The hidden closure environment param (`:envP`) is not a real C parameter:
# the environment is passed via the trailing `ClE_0` (added below) and
# `closureSetup` materialises `:envP` as a local cast of it. In a from-source
@@ -692,7 +692,7 @@ proc genProcParams(m: BModule; t: PType, rettype: var Rope, params: var Builder,
if isCompileTimeOnly(param.typ): continue
backendEnsureMutable param
fillParamName(m, param)
fillLoc(param.locImpl, locParam, t.n[i],
fillLoc(param.locImpl, locParam, child,
param.paramStorageLoc)
if isClosureEnv: continue # name/loc filled, but not part of the C signature
var typ: Rope
@@ -715,7 +715,7 @@ proc genProcParams(m: BModule; t: PType, rettype: var Rope, params: var Builder,
# need to pass hidden parameter:
params.addParam(paramBuilder, name = param.locImpl.snippet & "Len_" & $j, typ = NimInt)
inc(j)
arr = arr[0].skipTypes({tySink})
arr = arr.elementType.skipTypes({tySink})
if t.returnType != nil and isInvalidReturnType(m.config, t):
var arr = t.returnType
var typ: Snippet
@@ -742,18 +742,18 @@ proc mangleRecFieldName(m: BModule; field: PSym): Rope =
proc hasCppCtor(m: BModule; typ: PType): bool =
result = false
if m.compileToCpp and typ != nil and typ.itemId in m.g.graph.memberProcsPerType:
for prc in m.g.graph.memberProcsPerType[typ.itemId]:
if m.compileToCpp and typ != nil and typ.bindingId in m.g.graph.memberProcsPerType:
for prc in m.g.graph.memberProcsPerType[typ.bindingId]:
if sfConstructor in prc.flags:
return true
proc genCppParamsForCtor(p: BProc; call: PNode; didGenTemp: var bool): string
proc genCppParamsForCtor(p: BProc; call: AnyNode; didGenTemp: var bool): string
proc genCppInitializer(m: BModule, prc: BProc; typ: PType; didGenTemp: var bool): string =
#To avoid creating a BProc per test when called inside a struct nil BProc is allowed
result = "{}"
if typ.itemId in m.g.graph.initializersPerType:
let call = m.g.graph.initializersPerType[typ.itemId]
if typ.bindingId in m.g.graph.initializersPerType:
let call = m.g.graph.initializersPerType[typ.bindingId]
if call != nil:
var p = prc
if p == nil:
@@ -767,7 +767,7 @@ proc genRecordFieldsAux(m: BModule; n: PNode,
check: var IntSet; result: var Builder; unionPrefix = "") =
case n.kind
of nkRecList:
for ni in n.sons:
for ni in sons(n):
genRecordFieldsAux(m, ni, rectype, check, result, unionPrefix)
of nkRecCase:
if n.firstSon.kind != nkSym: internalError(m.config, n.info, "genRecordFieldsAux")
@@ -775,10 +775,10 @@ proc genRecordFieldsAux(m: BModule; n: PNode,
# prefix mangled name with "_U" to avoid clashes with other field names,
# since identifiers are not allowed to start with '_'
var unionBody = newBuilder("")
for i in 1..<n.len:
case n[i].kind
for i, it in isons(n, 1):
case it.kind
of nkOfBranch, nkElse:
let k = lastSon(n[i])
let k = lastSon(it)
if k.kind != nkSym:
let structName = "_" & mangleRecFieldName(m, n.firstSon.sym) & "_" & $i
var a = newBuilder("")
@@ -833,8 +833,8 @@ proc genMemberProcHeader(m: BModule; prc: PSym; result: var Builder; asPtr: bool
proc addRecordFields(result: var Builder; m: BModule; typ: PType, check: var IntSet) =
genRecordFieldsAux(m, typ.n, typ, check, result)
if typ.itemId in m.g.graph.memberProcsPerType:
let procs = m.g.graph.memberProcsPerType[typ.itemId]
if typ.bindingId in m.g.graph.memberProcsPerType:
let procs = m.g.graph.memberProcsPerType[typ.bindingId]
var isDefaultCtorGen, isCtorGen: bool = false
for prc in procs:
if sfConstructor in prc.flags:
@@ -915,7 +915,7 @@ proc resolveStarsInCppType(typ: PType, idx, stars: int): PType =
result = typ[idx]
for i in 1..stars:
if result != nil and result.kidsLen > 0:
result = if result.kind == tyGenericInst: result[FirstGenericParamAt]
result = if result.kind == tyGenericInst: result.firstGenericParam
else: result.elemType
proc getOpenArrayDesc(m: BModule; t: PType, check: var IntSet; kind: TypeDescKind): Rope =
@@ -1075,7 +1075,7 @@ proc getTypeDescAux(m: BModule; origTyp: PType, check: var IntSet; kind: TypeDes
let owner = hashOwner(t.sym)
if not gDebugInfo.hasEnum(t.sym.name.s, t.sym.info.line, owner):
var vals: seq[(string, int)] = @[]
for son in t.n.sons:
for son in sons(t.n):
assert(son.kind == nkSym)
let field = son.sym
vals.add((field.name.s, field.position.int))
@@ -1267,7 +1267,7 @@ proc genMemberProcHeader(m: BModule; prc: PSym; result: var Builder; asPtr: bool
var check = initIntSet()
fillBackendName(m, prc)
backendEnsureMutable prc
fillLoc(prc.locImpl, locProc, prc.ast[namePos], OnUnknown)
fillLoc(prc.locImpl, locProc, son(prc.ast, namePos), OnUnknown)
var memberOp = "#." #only virtual
var typ: PType
if isCtor:
@@ -1289,6 +1289,14 @@ proc genMemberProcHeader(m: BModule; prc: PSym; result: var Builder; asPtr: bool
name = typDesc
if isFnConst:
fnConst = " const"
if not isCtor:
# The call-site form (`x->salute(@)`), not the mangled Nim name. Set it on
# BOTH paths: whole-program cgen always emitted the out-of-class definition
# (the `else` branch) before any caller, but the per-module backend emits a
# foreign member proc's body in ITS OWN module, so the caller's TU only ever
# reaches the in-class declaration below — and called the member by the
# mangled name (`loo->salute_u0__vireouyks1()`, "struct Loo has no member").
prc.locImpl.snippet = "$1$2(@)" % [memberOp, name]
if isFwdDecl:
if isStatic:
result.add "static "
@@ -1298,9 +1306,7 @@ proc genMemberProcHeader(m: BModule; prc: PSym; result: var Builder; asPtr: bool
override = " override"
superCall = ""
else:
if not isCtor:
prc.locImpl.snippet = "$1$2(@)" % [memberOp, name]
elif superCall != "":
if isCtor and superCall != "":
superCall = " : " & superCall
name = "$1::$2" % [typDesc, name]
@@ -1315,7 +1321,7 @@ proc genProcHeader(m: BModule; prc: PSym; result: var Builder; visibility: var D
var check = initIntSet()
fillBackendName(m, prc)
backendEnsureMutable prc
fillLoc(prc.locImpl, locProc, prc.ast[namePos], OnUnknown)
fillLoc(prc.locImpl, locProc, son(prc.ast, namePos), OnUnknown)
var rettype: Snippet = ""
var desc = newBuilder("")
genProcParams(m, prc.typ, rettype, desc, check, true, false)
@@ -1456,7 +1462,7 @@ proc discriminatorTableName(m: BModule; objtype: PType, d: PSym): Rope =
# bugfix: we need to search the type that contains the discriminator:
var objtype = objtype.skipTypes(abstractPtrs)
while lookupInRecord(objtype.n, d.name) == nil:
objtype = objtype[0].skipTypes(abstractPtrs)
objtype = objtype.baseClass.skipTypes(abstractPtrs)
if objtype.sym == nil:
internalError(m.config, d.info, "anonymous obj with discriminator")
result = "NimDT_$1_$2" % [rope($hashType(objtype, m.config)), rope(d.name.s.mangle)]
@@ -1546,23 +1552,22 @@ proc genObjectFields(m: BModule; typ, origType: PType, n: PNode, expr: Rope;
else:
m.s[cfsData].addArrayVar(kind = Local, name = tmp,
elementType = ptrType("TNimNode"), len = toInt(L)+1)
for i in 1..<n.len:
var b = n[i] # branch
for b in sonsFrom(n, 1):
var tmp2 = getNimNode(m)
genObjectFields(m, typ, origType, lastSon(b), tmp2, info)
case b.kind
of nkOfBranch:
if b.len < 2:
internalError(m.config, b.info, "genObjectFields; nkOfBranch broken")
for j in 0..<b.len - 1:
if b[j].kind == nkRange:
var x = toInt(getOrdValue(b[j].firstSon))
var y = toInt(getOrdValue(b[j][1]))
for label in sonsButLast(b):
if label.kind == nkRange:
var x = toInt(getOrdValue(label.firstSon))
var y = toInt(getOrdValue(label.secondSon))
while x <= y:
m.s[cfsTypeInit3].addSubscriptAssignment(tmp, cIntValue(x), cAddr(tmp2))
inc(x)
else:
m.s[cfsTypeInit3].addSubscriptAssignment(tmp, cIntValue(getOrdValue(b[j])), cAddr(tmp2))
m.s[cfsTypeInit3].addSubscriptAssignment(tmp, cIntValue(getOrdValue(label)), cAddr(tmp2))
of nkElse:
m.s[cfsTypeInit3].addSubscriptAssignment(tmp, cIntValue(L), cAddr(tmp2))
else: internalError(m.config, n.info, "genObjectFields(nkRecCase)")
@@ -1780,7 +1785,7 @@ proc generateRttiDestructor(g: ModuleGraph; typ: PType; owner: PSym; kind: TType
dest.typ = getSysType(g, info, tyPointer)
result.typ = newProcType(info, idgen, owner)
result.typ = newProcType(info, idgen, result)
result.typ.addParam dest
var n = newNodeI(nkProcDef, info, bodyPos+1)
@@ -1856,7 +1861,7 @@ proc getObjDepth(t: PType): int16 =
result = -1
while x != nil:
x = skipTypes(x, skipPtrs)
x = x[0]
x = x.baseClass
inc(result)
proc genDisplayElem(d: MD5Digest): uint32 =
@@ -1872,7 +1877,7 @@ proc genDisplay(result: var Builder, m: BModule; t: PType, depth: int) =
while x != nil:
x = skipTypes(x, skipPtrs)
seqs[i] = cIntValue(genDisplayElem(MD5Digest(hashType(x, m.config))))
x = x[0]
x = x.baseClass
inc i
var arr: StructInitializer
@@ -1891,11 +1896,30 @@ proc genVTable(result: var Builder, seqs: seq[PSym]) =
result.add(cCast(CPointer, seqs[i].loc.snippet))
proc genTypeInfoV2OldImpl(m: BModule; t, origType: PType, name: Rope; info: TLineInfo) =
## The C++/HCR flavour: C++ has no designated initializers, so the RTTI record
## is a bare variable that the module's `DatInit` fills field by field.
cgsym(m, "TNimTypeV2")
m.s[cfsStrData].addDeclWithVisibility(Private):
m.s[cfsStrData].addVar(kind = Local, name = name, typ = "TNimTypeV2")
if m.config.cmd == cmdNifC:
# Same emit-everywhere split as `genTypeInfoV2Impl`: every `cg` process that
# demands this type declares it `extern`, and the DEFINITION is a droppable
# `'d'` unit the merge stage gives a single owner. Without the split the bare
# `TNimTypeV2 x;` in each TU is a tentative definition — which C's linker
# merges but C++'s does not, so `nim cpp --ic:on` died at link with
# "multiple definition of NTIv2__…". The field ASSIGNMENTS stay in every
# TU's `DatInit`: they are top-level code, not a definition, and every module
# computes the same values.
m.s[cfsStrData].addDeclWithVisibility(Extern):
m.s[cfsStrData].addVar(kind = Local, name = name, typ = "TNimTypeV2")
m.s[cfsVars].add(cnifDefDirective(name, "d", icNifName(m, origType)))
var def = newBuilder("")
def.addDeclWithVisibility(Private):
def.addVar(kind = Local, name = name, typ = "TNimTypeV2")
m.s[cfsVars].add extract(def)
m.s[cfsVars].add(cnifEndDefs())
m.icDataDefs.add (name, icNifName(m, origType))
else:
m.s[cfsStrData].addDeclWithVisibility(Private):
m.s[cfsStrData].addVar(kind = Local, name = name, typ = "TNimTypeV2")
var flags = 0
if not canFormAcycle(m.g.graph, t): flags = flags or 1
@@ -2070,7 +2094,7 @@ proc genTypeInfoV2(m: BModule; t: PType; info: TLineInfo): Rope =
result = "NTIv2$1_" % [rope($sig)]
m.typeInfoMarkerV2[sig] = result
let owner = t.skipTypes(typedescPtrs).itemId.module
let owner = t.skipTypes(typedescPtrs).bindingId.module
# In the per-module backend (`cg`) RTTI is emit-everywhere like procs and
# consts: every demanding module emits the `'d'` definition (deduped to one
# owner by the merge stage). The owner-routing below would instead push the
@@ -2173,7 +2197,7 @@ proc genTypeInfoV1(m: BModule; t: PType; info: TLineInfo): Rope =
declareNimType(m, "TNimType", result, old.int)
return prefixTI(result)
var owner = t.skipTypes(typedescPtrs).itemId.module
var owner = t.skipTypes(typedescPtrs).bindingId.module
# In the per-module backend (`cg`) V1 RTTI is emit-everywhere like procs,
# consts and V2 type info: every demanding module emits the `'d'` definition
# (deduped to one owner by the merge stage). The owner-routing below would
@@ -2265,7 +2289,7 @@ proc genTypeInfo*(config: ConfigRef, m: BModule; t: PType; info: TLineInfo): Rop
proc retrieveSym(n: PNode): PSym =
case n.kind
of nkPostfix: result = retrieveSym(n[1])
of nkPostfix: result = retrieveSym(n.secondSon)
of nkPragmaExpr, nkTypeDef: result = retrieveSym(n.firstSon)
of nkSym: result = n.sym
else: result = nil
@@ -2289,8 +2313,8 @@ proc genTypeSection(m: BModule, n: PNode) =
# declarations where the type is already written separately before the initializer.
proc genCppConstructorExpr(m: BModule, prc: BProc; typ: PType; didGenTemp: var bool): Snippet =
var params = ""
if typ.itemId in m.g.graph.initializersPerType:
let call = m.g.graph.initializersPerType[typ.itemId]
if typ.bindingId in m.g.graph.initializersPerType:
let call = m.g.graph.initializersPerType[typ.bindingId]
if call != nil:
var p = prc
if p == nil:

View File

@@ -11,7 +11,7 @@
import
ast, types, msgs, wordrecg,
platform, trees, options, cgendata, mangleutils, renderer, modulegraphs
platform, trees, options, cgendata, mangleutils, renderer, modulegraphs, bnode
import std/[hashes, strutils, formatfloat]
@@ -22,18 +22,38 @@ proc getPragmaStmt*(n: PNode, w: TSpecialWord): PNode =
case n.kind
of nkStmtList:
result = nil
for i in 0..<n.len:
result = getPragmaStmt(n[i], w)
for it in sons(n):
result = getPragmaStmt(it, w)
if result != nil: break
of nkPragma:
result = nil
for i in 0..<n.len:
if whichPragma(n[i]) == w: return n[i]
for it in sons(n):
if whichPragma(it) == w: return it
else:
result = nil
proc stmtsContainPragma*(n: PNode, w: TSpecialWord): bool =
result = getPragmaStmt(n, w) != nil
proc stmtsContainPragma*(n: AnyNode, w: TSpecialWord): bool =
## Deliberately NOT `getPragmaStmt(n, w) != nil`, and the reason is the one
## shape the `AnyNode` seam cannot serve: a proc that returns a node OR nil.
## `.bif` spells a missing child as a `DotToken` *inside* a tree, so there is
## no nil token to hand back as a return value, and a `Cursor` is not nilable.
## Predicates split out from such a proc are the way across.
##
## The duplicated traversal is the cost, and it is checked rather than
## trusted: `grindPredicates` asserts this answers exactly
## `getPragmaStmt(n, w) != nil` at every node, so the two cannot drift apart
## silently.
case n.kind
of nkStmtList:
result = false
for it in sons(n):
if stmtsContainPragma(it, w): return true
of nkPragma:
result = false
for it in sons(n):
if whichPragma(it) == w: return true
else:
result = false
proc hashString*(conf: ConfigRef; s: string): BiggestInt =
# has to be the same algorithm as strmantle.hashString!
@@ -92,7 +112,7 @@ proc ccgIntroducedPtr*(conf: ConfigRef; s: PSym, retType: PType): bool =
result = true
elif (optByRef in s.options) or (getSize(conf, pt) > conf.target.floatSize * 3):
result = true # requested anyway
elif (tfFinal in pt.flags) and (pt[0] == nil):
elif (tfFinal in pt.flags) and (pt.baseClass == nil):
result = false # no need, because no subtyping possible
else:
result = true # ordinary objects are always passed by reference,
@@ -113,20 +133,12 @@ proc encodeName*(name: string): string =
proc makeUnique(m: BModule; s: PSym, name: string = ""): string =
result = if name == "": s.name.s else: name
# keep backend-minted ids out of the `_u` namespace; their item counter
# restarts at 0 and would collide with loaded symbols' ids
# restarts at 0 and would collide with loaded symbols' ids. Which integer
# identifies such a symbol is decided ONCE, in `astdef.backendMintedDisamb`,
# shared with `mangleProcNameExt` and `ast2nif.toNifSymName`.
if s.itemId.isBackendMinted:
result.add "_c"
if (s.disamb and HookDisambBit) != 0'i32:
# A backend-minted sym whose `disamb` is content-derived (setHookDisamb gave
# it HookDisambBit) — e.g. the `rttiDestroy` wrapper. Its `itemId.item` is a
# PER-PROCESS backend counter, so using it makes the C name diverge across
# the emit-everywhere processes: the type's RTTI table (emit-everywhere,
# merge-deduped) ends up referencing one process's `_c<item>` while the
# wrapper is defined with another's -> undefined at link (`rttiDestroy_c23`).
# The content-derived disamb is stable across processes, so use it.
result.add $s.disamb
else:
result.add $s.itemId.item
result.add $backendMintedDisamb(s)
else:
result.add "_u"
# Mirror `mangleProcNameExt`: use the per-(module,name) `disamb`, NOT
@@ -156,7 +168,7 @@ proc encodeType*(m: BModule; t: PType; staticLists: var string): string =
of tyObject, tyEnum, tyDistinct, tyUserTypeClass, tyGenericParam:
result = encodeSym(m, t.sym)
of tyGenericInst, tyUserTypeClassInst, tyGenericBody:
result = encodeName(t[0].sym.name.s)
result = encodeName(t.genericHead.sym.name.s)
result.add "I"
for i in 1..<t.len - 1:
result.add encodeType(m, t[i], staticLists)
@@ -168,8 +180,7 @@ proc encodeType*(m: BModule; t: PType; staticLists: var string): string =
of tySequence: encodeName("seq")
else: encodeName(kindName)
result.add "I"
for i in 0..<t.len:
let s = t[i]
for s in kids(t):
if s.isNil: continue
result.add encodeType(m, s, staticLists)
result.add "E"
@@ -180,12 +191,12 @@ proc encodeType*(m: BModule; t: PType; staticLists: var string): string =
raiseAssert "unreachable"
of tyRange:
var val = "range_"
if t.n[0].typ.kind in {tyFloat..tyFloat128}:
val.addFloat t.n[0].floatVal
if t.n.firstSon.typ.kind in {tyFloat..tyFloat128}:
val.addFloat t.n.firstSon.floatVal
val.add "_"
val.addFloat t.n[1].floatVal
val.addFloat t.n.secondSon.floatVal
else:
val.add $t.n[0].intVal & "_" & $t.n[1].intVal
val.add $t.n.firstSon.intVal & "_" & $t.n.secondSon.intVal
result = encodeName(val)
of tyString..tyUInt64, tyPointer, tyBool, tyChar, tyVoid, tyAnything, tyNil, tyEmpty:
result = encodeName(kindName)

File diff suppressed because it is too large Load Diff

View File

@@ -142,6 +142,13 @@ type
# not a list of IDs nor can it be made to be one.
mangledPrcs*: HashSet[string]
icEmitted*: IntSet
## Under `--icBackendStage:cg`: the positions of the modules THIS process
## writes a translation unit for. `cgen.findPendingModule` consults it to
## decide where a demanded definition goes — see the comment there. Empty
## outside that stage, which is why every other backend keeps the ordinary
## whole-program routing.
TCGen = object of PPassContext # represents a C source file
s*: TCFileSections # sections of the C file
flags*: set[CodegenFlag]
@@ -186,6 +193,10 @@ type
# embeds (redirected defs, shared instances,
# hooks); recorded as the artifact's cdeps so
# the reuse gate can check their impl cookies
icGlobalDtorName*: string # per-module backend: the C name of this
# module's global-destructor proc, recorded in
# the artifact's meta head so the main module's
# `cg` — a different process — can call it
icDataDefs*: seq[tuple[cname, nifname: string]]
# C names of data definitions (consts, globals,
# RTTI) this TU embeds plus their NIF symbol
@@ -234,7 +245,8 @@ proc newProc*(prc: PSym, module: BModule): BProc =
proc newModuleList*(g: ModuleGraph): BModuleList =
BModuleList(typeInfoMarker: initTable[SigHash, tuple[str: Rope, owner: int32]](),
config: g.config, graph: g, nimtvDeclared: initIntSet())
config: g.config, graph: g, nimtvDeclared: initIntSet(),
icEmitted: initIntSet())
iterator cgenModules*(g: BModuleList): BModule =
for m in g.modulesClosed:

View File

@@ -197,10 +197,10 @@ proc methodDef*(g: ModuleGraph; idgen: IdGenerator; s: PSym) =
if witness.isNil: witness = g.methods[i].methods[0]
# create a new dispatcher:
# stores the id and the position
if s.typ.firstParamType.skipTypes(skipPtrs).itemId notin g.bucketTable:
g.bucketTable[s.typ.firstParamType.skipTypes(skipPtrs).itemId] = 1
if s.typ.firstParamType.skipTypes(skipPtrs).bindingId notin g.bucketTable:
g.bucketTable[s.typ.firstParamType.skipTypes(skipPtrs).bindingId] = 1
else:
g.bucketTable.inc(s.typ.firstParamType.skipTypes(skipPtrs).itemId)
g.bucketTable.inc(s.typ.firstParamType.skipTypes(skipPtrs).bindingId)
g.methods.add((methods: @[s], dispatcher: createDispatcher(s, g, idgen)))
logMethodDef(g, s)
#echo "adding ", s.info

View File

@@ -73,14 +73,15 @@ proc stripCnifMarks*(s: string): string =
inc i
const
CnifVersion* = "4"
CnifVersion* = "5"
## Artifact format version, stored in the meta head. Artifacts written
## by an older compiler lack the NIF names and the cref group the
## def-retention check needs (v2), the cdeps group the fine-grained
## reuse gate needs (v3), or the type NIF names and cnif-marked extern
## reuse gate needs (v3), the type NIF names and cnif-marked extern
## RTTI references the typeinfo flavor of the def-retention check
## needs (v4); `readCnifHeads` reports them as invalid so their TUs
## simply regenerate once.
## needs (v4), or the global-destructor name the main module's `cg`
## calls at teardown (v5); `readCnifHeads` reports them as invalid so
## their TUs simply regenerate once.
proc cnifDefDirective*(name, flags, nifName: string): string =
CnifDefStart & name & CnifDefSep & flags & CnifDefSep & nifName & CnifDefEnd
@@ -91,15 +92,17 @@ proc cnifEndDefs*(): string =
proc writeCnifArtifact*(code: string; outfile: string;
initRequired = false; datInitRequired = false;
dataDefs: openArray[tuple[cname, nifname: string]] = [];
semmedNif = ""; moduleBase = "";
semmedNif = ""; moduleBase = ""; globalDtor = "";
implDeps: openArray[string] = []) =
## Splits the marked module text into the `.c.nif` artifact.
## The artifact starts with a `(meta <flags> "semmedNif" "moduleBase"
## "version")` head — whether the module has an init/datInit proc
## ('i'/'d'), which semmed NIF it was generated from and the module's
## "version" "globalDtor")` head — whether the module has an init/datInit
## proc ('i'/'d'), which semmed NIF it was generated from, the module's
## mangled base name (what `registerModuleToMain` and the reuse decision
## need when the TU is reused in a later run, possibly without the module
## ever being loaded again) — a `(cdata (SymbolDef StrLit)*)` group naming
## ever being loaded again) and the C name of the module's global-destructor
## proc, if any (what the main module's `cg` calls at program teardown; see
## `cgen.genIcModuleDestroyGlobals`) — a `(cdata (SymbolDef StrLit)*)` group naming
## the data definitions (consts, globals, RTTI) the TU embeds together
## with their NIF names, a `(cref Ident*)` group naming every C name
## the TU references but does not define itself (what the def-retention
@@ -153,6 +156,7 @@ proc writeCnifArtifact*(code: string; outfile: string;
b.addStrLit semmedNif
b.addStrLit moduleBase
b.addStrLit CnifVersion
b.addStrLit globalDtor
b.withTree "cdata":
for d in dataDefs:
b.addSymbolDef d.cname
@@ -261,6 +265,8 @@ type
datInitRequired*: bool
semmedNif*: string ## the semmed NIF this TU was generated from
moduleBase*: string ## the module's mangled base name
globalDtor*: string ## C name of the module's global-destructor proc
## ("" when the module has no global destructors)
cdefs*: seq[tuple[cname, nifname: string]] ## the proc definitions
cdata*: seq[tuple[cname, nifname: string]] ## the data definitions
crefs*: seq[string] ## C names referenced but not defined here
@@ -303,6 +309,7 @@ proc readCnifHeads*(f: string): CnifHeads =
if strIdx == 0: result.semmedNif = strVal(c)
elif strIdx == 1: result.moduleBase = strVal(c)
elif strIdx == 2: version = strVal(c)
elif strIdx == 3: result.globalDtor = strVal(c)
inc strIdx
inc c
else:
@@ -585,6 +592,13 @@ proc computeMergeDecision*(files: openArray[string]): MergeDecision =
if d in result.live: inc result.liveDefs
const MergeDecisionFile* = "ic.backend.merge.nif"
const LiveModulesFile* = "ic.backend.live.txt"
## One `.c.nif` path per line: exactly the artifacts of the modules the CURRENT
## build graph considers live. The `merge` stage reads this instead of globbing
## `*.c.nif` off the nimcache, so a leftover artifact from an unrelated build
## that happens to share the cache directory cannot be merged in (which is what
## made a shared prebuilt cache unusable: merge picked owners in modules the
## program does not import, and the link then wanted their objects).
## Fixed name of the merge stage's output in the nimcache, read by `emit`.
proc writeMergeDecision*(outfile: string; d: MergeDecision) =

View File

@@ -627,7 +627,11 @@ proc processMemoryManagementOption(switch, arg: string, pass: TCmdLinePass,
conf.selectedGC = gcHooks
defineSymbol(conf.symbols, "gchooks")
incl conf.globalOptions, optSeqDestructors
processOnOffSwitchG(conf, {optSeqDestructors}, arg, pass, info)
# (The `arg` here is the mm MODE — "hooks" — so feeding it to an on/off
# switch made `--mm:hooks` fail outright with "'on' or 'off' expected, but
# 'hooks' found". The `incl` above is what that call was meant to do.
# Reachable only via the explicit switch: `--newruntime` sets
# `selectedGC` directly, which is why this stayed hidden.)
if pass in {passCmd2, passPP}:
defineSymbol(conf.symbols, "nimSeqsV2")
of "go":
@@ -985,12 +989,16 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
expectArg(conf, switch, arg, pass, info)
if pass in {passCmd2, passPP}:
conf.icBackendStage = arg
of "icbackendmodule":
# `nim nifc` only: the NIF module suffix the cg/emit stage operates on (see
# options.icBackendModule).
of "icbackendmodule", "icbackendmodules":
# `nim nifc` only: the NIF module suffixes the lower/cg/emit stage operates
# on, comma-separated — the invocation's batch (see
# options.icBackendModules). The singular spelling is the same switch: a
# one-module batch is what the per-module fan-out passes.
expectArg(conf, switch, arg, pass, info)
if pass in {passCmd2, passPP}:
conf.icBackendModule = arg
conf.icBackendModules = @[]
for suffix in arg.split(','):
if suffix.len > 0: conf.icBackendModules.add suffix
of "import":
expectArg(conf, switch, arg, pass, info)
if pass in {passCmd2, passPP}:
@@ -1088,9 +1096,14 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
expectNoArg(conf, switch, arg, pass, info)
helpOnError(conf, pass)
of "symbolfiles", "incremental", "ic":
if switch.normalize == "symbolfiles": deprecatedAlias(switch, "incremental")
if pass in {passCmd2, passPP} and switch.normalize == "symbolfiles":
deprecatedAlias(switch, "incremental")
# xxx maybe also ic, since not in help?
if pass in {passCmd2, passPP}:
# `--ic:on` is read in passCmd1 too: `nim.nim` decides BEFORE config loading
# whether this run is an IC driver (`ensureIcConfig` must produce the
# precompiled config the driver itself then replays), and passCmd1 is the
# only pass that has run by then.
if pass in {passCmd1, passCmd2, passPP}:
case arg.normalize
of "on": conf.ic = true
of "legacy": conf.symbolFiles = v2Sf

View File

@@ -11,7 +11,8 @@
## for details. Note this is a first implementation and only the "Concept matching"
## section has been implemented.
import ast, semdata, lookups, lineinfos, idents, msgs, renderer, types, layeredtable
import ast, semdata, lookups, lineinfos, idents, msgs, renderer, types,
layeredtable, semtypinst
import std/sets
@@ -71,7 +72,8 @@ proc semConceptDeclaration*(c: PContext; n: PNode): PNode =
type
MatchFlags* = enum
mfDontBind # Do not bind generic parameters
mfDontBind # Do not export bindings from the concept match
mfBindGenericParam # Export inferred invocation parameters despite mfDontBind
mfCheckGeneric # formal <- formal comparison as opposed to formal <- operand
ConceptTypePair = tuple[conceptId, typeId: ItemId]
@@ -205,7 +207,7 @@ proc matchConceptToImpl(c: PContext, f, potentialImpl: PType; m: var MatchCon):
# Cycle detection: track (concept, type) pairs to prevent infinite recursion.
# Returns true on cycle (coinductive semantics) to support co-dependent concepts.
let pair: ConceptTypePair = (concpt.itemId, potentialImpl.itemId)
let pair: ConceptTypePair = (concpt.bindingId, potentialImpl.bindingId)
if pair in m.marker:
return true
m.marker.incl pair
@@ -573,7 +575,17 @@ proc conceptMatchNode(c: PContext; n: PNode; m: var MatchCon): bool =
# error was reported earlier.
result = false
proc fixBindings(bindings: var LayeredIdTable; concpt: PType; invocation: PType; m: var MatchCon) =
proc resolvedBinding(c: PContext; t: PType; m: MatchCon): PType =
## An inferred concept parameter can refer to an implementation-local
## generic parameter, for example `Elem[Impl.T]`. Resolve it while the
## matcher's private bindings (`Impl.T -> int`) are still available.
if t.containsUnresolvedType:
prepareMetatypeForSigmatch(c, m.bindings, m.concpt.sym.info, t)
else:
t
proc fixBindings(c: PContext; bindings: var LayeredIdTable; concpt: PType;
invocation: PType; m: var MatchCon) =
# invocation != nil means we have a non-atomic concept:
if invocation != nil and invocation.kind == tyGenericInvocation:
assert concpt.sym.typ.kind == tyGenericBody
@@ -585,8 +597,9 @@ proc fixBindings(bindings: var LayeredIdTable; concpt: PType; invocation: PType;
continue
let found = m.bindings.lookup(thisSym)
if found != nil:
when logBindings: echo "Invocation bind: ", thisSym, " ", found
bindings.put(thisSym, found)
let resolved = resolvedBinding(c, found, m)
when logBindings: echo "Invocation bind: ", thisSym, " ", resolved
bindings.put(thisSym, resolved)
# bind even more generic parameters
let genBody = invocation.base
@@ -602,6 +615,20 @@ proc fixBindings(bindings: var LayeredIdTable; concpt: PType; invocation: PType;
bindings.put(invocation[i], boundV)
bindings.put(concpt, m.potentialImplementation)
proc fixConstraintBindings(c: PContext; bindings: var LayeredIdTable;
invocation: PType; m: MatchCon) =
## Propagates only the dependent parameters of a concept constraint. The
## concept itself and its private matcher bindings must remain unbound so
## that independent constraints using the same concept don't get coupled.
if invocation != nil and invocation.kind == tyGenericInvocation:
let genBody = invocation.base
assert genBody.kind == tyGenericBody
for i in FirstGenericParamAt ..< invocation.kidsLen:
if lookup(bindings, invocation[i]) == nil:
let boundValue = m.bindings.lookup(genBody[i - 1])
if boundValue != nil:
bindings.put(invocation[i], resolvedBinding(c, boundValue, m))
proc processConcept(c: PContext; concpt, invocation: PType, bindings: var LayeredIdTable; m: var MatchCon): bool =
m.bindings = m.bindings.newTypeMapLayer()
if invocation != nil and invocation.kind == tyGenericInst:
@@ -611,8 +638,11 @@ proc processConcept(c: PContext; concpt, invocation: PType, bindings: var Layere
if invocation[i].kind != tyVoid:
bindParam(c, m, genericBody[i-1], invocation[i])
result = conceptMatchNode(c, concpt.conceptBody, m)
if result and mfDontBind notin m.flags:
fixBindings(bindings, concpt, invocation, m)
if result:
if mfDontBind notin m.flags:
fixBindings(c, bindings, concpt, invocation, m)
elif mfBindGenericParam in m.flags:
fixConstraintBindings(c, bindings, invocation, m)
proc conceptMatch*(c: PContext; concpt, arg: PType; bindings: var LayeredIdTable; invocation: PType, flags: set[MatchFlags] = {}): bool =
## Entry point from sigmatch. 'concpt' is the concept we try to match (here still a PType but

View File

@@ -11,12 +11,15 @@
## This enables incremental and parallel compilation using the `m` switch.
import std / [os, tables, sets, times, osproc, algorithm, strtabs, strutils, syncio]
from std/sha1 import secureHash, `$`
import options, msgs, lineinfos, pathutils, condsyms,
modulepaths, extccomp, cnif, platform
import "../dist/nimony/src/lib" / [nifstreams, bitabs, nifreader, nifbuilder]
import nifstreams
import "../dist/nimony/src/lib" / [bitabs, nifreader, nifbuilder]
import icmodnames
import icnifcore
from ic/replayer import BackendActionsExt
type
FilePair = object
@@ -26,6 +29,14 @@ type
Node = ref object
files: seq[FilePair] # main file + includes
deps: seq[int] # indices into DepContext.nodes
specDeps: seq[int] # the subset of `deps` reached ONLY through a `when`
# condition the scanner could not evaluate
missingImport: string # an `import` path this module's source names, under a
# `when` the scanner could not decide, that does not
# exist on disk (empty when all resolved)
missingHardImport: string ## ditto but NOT under any undecidable `when`: the
## real compile would reach this `import`, so it is
## a genuine "cannot open file" error
id: int
DepContext = object
@@ -41,6 +52,9 @@ type
scanningMain: bool # currently scanning the project main module's deps;
# makes `when isMainModule` conditions evaluate true
# only there (every other module is imported)
speculating: int # nesting depth of `when` guards the scanner could not
# decide; every import edge added while this is > 0 is
# recorded as speculative (see pruneDeadSpeculative)
proc toPair(c: DepContext; f: string): FilePair =
FilePair(nimFile: f, modname: moduleSuffix(f, cast[seq[string]](c.config.searchPaths)))
@@ -51,6 +65,13 @@ proc depsFile(c: DepContext; f: FilePair): string =
proc parsedFile(c: DepContext; f: FilePair): string =
getNimcacheDir(c.config).string / f.modname & ".p.nif"
proc parsedDepsFile(c: DepContext; f: FilePair): string =
## The deps sidecar `nifler parse --deps <src> <out>.p.nif` actually writes: it
## appends `.deps.nif` to the OUTPUT path, giving `<mod>.p.deps.nif`. Not to be
## confused with `depsFile` (`<mod>.deps.nif`), which the driver's own
## `nifler deps` pre-scan writes.
parsedFile(c, f).changeFileExt("") & ".deps.nif"
proc semmedFile(c: DepContext; f: FilePair): string =
getNimcacheDir(c.config).string / f.modname & ".s.bif"
@@ -206,6 +227,18 @@ proc getsImplicitImports(c: DepContext; nimFile: string): bool =
## system.nim and never reaches them). Stdlib == under conf.libpath.
not isRelativeTo(nimFile, c.config.libpath.string)
proc addDepEdge(c: DepContext; current: Node; depId: int) =
## Record `current -> depId`. While the scanner is inside a `when` guard it
## could not evaluate (`c.speculating > 0`) the edge is *speculative*: it may
## not exist in the real compile at all. An edge seen at least once outside
## such a guard is hard and stays hard.
if depId notin current.deps: current.deps.add depId
if c.speculating > 0:
if depId notin current.specDeps: current.specDeps.add depId
else:
let i = current.specDeps.find(depId)
if i >= 0: current.specDeps.delete i
proc processImport(c: var DepContext; importPath: string; current: Node; origin: string) =
# `origin` = the file the `import` literally appears in. Crucial for imports
# inside `include`d files: e.g. `system.nim` includes `system/excpt.nim`, which
@@ -217,6 +250,14 @@ proc processImport(c: var DepContext; importPath: string; current: Node; origin:
# only after the post-sem `.s.deps` revealed the edge.
let resolved = resolveImport(c, origin, importPath)
if resolved.len == 0 or not fileExists(resolved):
# The module does not exist on disk. Silently ignoring this is right for the
# scanner (the `import` may sit in a dead `when` branch and the real compile
# never looks at it), but remember it: `pruneDeadSpeculative` uses it to tell
# a module that is merely unused apart from one that cannot compile at all.
if c.speculating > 0:
if current.missingImport.len == 0: current.missingImport = importPath
elif current.missingHardImport.len == 0:
current.missingHardImport = importPath
return
let pair = c.toPair(resolved)
@@ -225,7 +266,7 @@ proc processImport(c: var DepContext; importPath: string; current: Node; origin:
if existingIdx == -1:
# New module - create node and process it
let newNode = Node(files: @[pair], id: c.nodes.len)
current.deps.add newNode.id
addDepEdge(c, current, newNode.id)
# Every module depends on system.nim
if c.systemNodeId >= 0:
newNode.deps.add c.systemNodeId
@@ -243,8 +284,7 @@ proc processImport(c: var DepContext; importPath: string; current: Node; origin:
traverseDeps(c, pair, newNode)
else:
# Already processed - just add dependency
if existingIdx notin current.deps:
current.deps.add existingIdx
addDepEdge(c, current, existingIdx)
proc skipSubtree(s: var Stream; first: PackedToken) =
## Consume tokens until the ParLe at `first` is balanced. Caller has
@@ -482,6 +522,18 @@ proc parseImportPath(s: var Stream; t: var PackedToken): seq[string] =
for r in parseImportPath(s, t):
result.add op & r
if t.kind == ParRi: t = next(s) # skip closing ')'
elif tag == "pragmax":
# `import x {.all.}` serialises as `(pragmax x (pragmas all))`. Without
# this it fell into the unknown-subtree skip below and the import was
# DROPPED from the static graph: the build only learned about it from the
# `.s.deps` sidecar a round later, after a round that failed with
# "requires precompiled NIF for import". Correct, but a wasted round and
# an alarming error line for an ordinary import.
t = next(s) # skip 'pragmax' tag
result = parseImportPath(s, t) # the path is the first child
while t.kind != ParRi and t.kind != EofToken:
discard parseImportPath(s, t) # the pragma list; consumed, not a path
if t.kind == ParRi: t = next(s) # skip closing ')'
elif tag == "bracket":
t = next(s) # skip 'bracket' tag
while t.kind != ParRi and t.kind != EofToken:
@@ -533,14 +585,19 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) =
# entirely. Otherwise advance past the marker and parse the path.
t = next(s)
var live = true
var speculative = false
if t.kind == ParLe and pool.tags[t.tagId] == "when":
# whenMarkerHolds consumes everything up to and including the
# closing `)` of the `(when ...)` subtree. Drop the import only when
# the condition is PROVABLY false; a `cvUnknown` condition (e.g. an
# `else:` branch guarded by `not <unevaluatable call>`, as in
# `when tryImport x: ... else: import x`) keeps the dependency so the
# static graph never misses a real import.
live = whenMarkerHolds(c, s) != cvFalse
# static graph never misses a real import — but marks every edge it
# creates speculative, so `pruneDeadSpeculative` can still drop a
# subtree that provably cannot compile in this configuration.
let cond = whenMarkerHolds(c, s)
live = cond != cvFalse
speculative = cond == cvUnknown
t = next(s)
if not live:
# Drain the rest of this import/include node.
@@ -558,6 +615,7 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) =
# that expand to several imports. A plain `import a, b, c` lists several
# modules as siblings; a `fromimport` has a single path followed by the
# imported symbol list, which must not be treated as modules.
if speculative: inc c.speculating
if tag == "fromimport" or tag == "importexcept":
# `from m import syms` / `import m except syms`: the first child is the
# module path; the rest is the (in/ex)cluded symbol list, which must not
@@ -573,6 +631,7 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) =
processInclude(c, importPath, current, pair.nimFile)
else:
processImport(c, importPath, current, pair.nimFile)
if speculative: dec c.speculating
# Drain any remaining tokens of this node (e.g. the symbol list of a
# `fromimport`), up to and including the node's closing ')'.
var depth = 1
@@ -689,6 +748,148 @@ proc traverseDeps(c: var DepContext; pair: FilePair; current: Node) =
return
readDepsFile(c, pair, current)
proc pruneDeadSpeculative(c: var DepContext) =
## Drop modules that are reachable only through a `when` guard the scanner
## cannot evaluate AND that cannot possibly compile because they import a
## module which does not exist on disk.
##
## The motivating shape is the ordinary `{.strdefine.}` backend switch:
##
## const figdrawTextBackend* {.strdefine.} = "pixie"
## when figdrawTextBackend == "harfbuzzy":
## import ./textrasters/glyphid_raster # imports `pkg/harfbuzzy`
##
## The value of that const needs sem, so `evalCondCmp` answers `cvUnknown` and
## the conservative rule keeps the import — the right call for an edge, but it
## also gives `glyphid_raster` its own `nim m` rule. The classic compiler never
## looks at that file; IC compiles it, cannot find `pkg/harfbuzzy`, and the
## whole build dies on a package the user never installed because they never
## selected that backend.
##
## Dropping is safe: if the guard *was* live, the importer's own `nim m` fails
## on the missing NIF, records the import in its `.s.deps` sidecar, and the
## discovery fixpoint re-adds the node — this time reporting the honest
## `cannot open file: pkg/harfbuzzy/raw` instead of a cascade of
## `undeclared identifier` noise.
let n = c.nodes.len
if n == 0: return
var roots = @[0]
if c.systemNodeId >= 0: roots.add c.systemNodeId
for i in c.implicitNodeIds: roots.add i
# Reachability through NON-speculative edges only: these modules are compiled
# for certain, so a missing import in them is a genuine user error to report.
var hard = newSeq[bool](n)
var stack = roots
while stack.len > 0:
let v = stack.pop()
if hard[v]: continue
hard[v] = true
for d in c.nodes[v].deps:
if d notin c.nodes[v].specDeps and not hard[d]: stack.add d
# A module the real compile DOES reach, naming an import that is not on disk,
# is a plain user error — and one nifmake cannot notice on its own: deleting
# `effects.nim` moves no mtime, so the importer's `nim m` never re-fires and
# `nim ic` happily relinked a stale binary while `nim c` said "cannot open
# file". Report it here, where the graph scan is the only thing that looks at
# import paths at all.
var reported = false
for i in 0 ..< n:
if hard[i] and c.nodes[i].missingHardImport.len > 0:
rawMessage(c.config, errGenerated,
c.nodes[i].files[0].nimFile & ": cannot open file: " &
c.nodes[i].missingHardImport)
reported = true
if reported: return
var dead = newSeq[bool](n)
var anyDead = false
for i in 0 ..< n:
if not hard[i] and c.nodes[i].missingImport.len > 0:
dead[i] = true
anyDead = true
if not anyDead: return
# Anything left reachable only through a dead node is dead too.
var alive = newSeq[bool](n)
stack = @[]
for r in roots:
if not dead[r]: stack.add r
while stack.len > 0:
let v = stack.pop()
if alive[v]: continue
alive[v] = true
for d in c.nodes[v].deps:
if not dead[d] and not alive[d]: stack.add d
# Drop the scan artifacts of a module that just left the graph, so an
# edit-accumulated cache does not differ from a clean one for no reason
# (`tests/ic/tdead_when_import` pins that). Re-running nifler if it ever comes
# back costs a single parse.
#
# But a FILE can belong to several nodes, and only the NODE is dead.
# `lib/system/inclrtl.nim` is `include`d by dozens of live stdlib modules and
# also sits in the file set of a dead-speculative one; a clean build therefore
# has its `.p.nif`, and deleting it here does not tidy the cache, it corrupts
# it. The consequences compound: the missing output re-fires that file's
# `nifler` rule, which rewrites the parsed file with a fresh mtime, which
# re-fires every `nim_m` rule listing it as an input — 16 full module re-sems
# (system, os, times, strutils, macros, unicode, ...) on every warm build, for
# ever, because the scanner is stateless and rediscovers the dead node each
# run. Measured on a 219-module program: an 11 s NO-OP build. So delete only
# what no live node claims.
var liveFiles = initHashSet[string]()
for i in 0 ..< n:
if alive[i]:
for f in c.nodes[i].files: liveFiles.incl f.nimFile
var cascaded = 0
for i in 0 ..< n:
if not alive[i]:
for f in c.nodes[i].files:
if f.nimFile in liveFiles: continue
removeFile(c.parsedFile(f))
removeFile(c.depsFile(f))
removeFile(c.parsedDepsFile(f))
if c.nodes[i].missingImport.len > 0:
rawMessage(c.config, hintSuccess,
"ic: skipping " & c.nodes[i].files[0].nimFile &
" (reached only under an undecidable `when`, and imports " &
c.nodes[i].missingImport & ", which is not installed)")
else:
inc cascaded
if cascaded > 0:
rawMessage(c.config, hintSuccess,
"ic: " & $cascaded & " further module(s) skipped, reachable only through those")
# Compact `c.nodes`; node ids ARE indices everywhere, so remap them all.
var remap = newSeq[int](n)
var newNodes: seq[Node] = @[]
for i in 0 ..< n:
if alive[i]:
remap[i] = newNodes.len
newNodes.add c.nodes[i]
else:
remap[i] = -1
proc remapped(remap: seq[int]; src: seq[int]): seq[int] =
result = @[]
for x in src:
if remap[x] >= 0 and remap[x] notin result: result.add remap[x]
for node in newNodes:
node.id = remap[node.id]
node.deps = remapped(remap, node.deps)
node.specDeps = remapped(remap, node.specDeps)
c.nodes = newNodes
var pm = initTable[string, int]()
for name, idx in c.processedModules:
if idx >= 0 and idx < n and remap[idx] >= 0: pm[name] = remap[idx]
c.processedModules = pm
if c.systemNodeId >= 0: c.systemNodeId = remap[c.systemNodeId]
c.implicitNodeIds = remapped(remap, c.implicitNodeIds)
proc computeSCCs(c: DepContext): seq[seq[int]] =
## Tarjan's strongly-connected-components over the module dependency graph
## (`node.deps`). Each returned component is a list of node indices; a module
@@ -771,6 +972,19 @@ proc computeForwardedArgs(c: DepContext): seq[string] =
# them — phantom outputs that re-fire the build on every rerun).
if c.config.selectedGC != gcUnselected:
result.add "--mm:" & $c.config.selectedGC
# The children are invoked as `nim m` / `nim nifc`, so the driver's own command
# token (`c`, `cpp`, `ic`) is gone and with it the backend it selected. Name it
# explicitly — `nim cpp --ic:on` must not have its stdlib sem'd and its TUs
# emitted as C. The exception model rides along for the same reason: `nim cpp`
# defaults to `--exceptions:cpp`, which changes both codegen and sem.
if c.config.backend != backendInvalid:
result.add "--backend:" & $c.config.backend
if c.config.exc != excNone:
result.add "--exceptions:" & (case c.config.exc
of excGoto: "goto"
of excCpp: "cpp"
of excQuirky: "quirky"
else: "setjmp")
# method dispatch semantics must match across the child processes:
# a child compiled without --multimethods:on builds different dispatch
# buckets (and rejects calls as ambiguous that multi-dispatch accepts)
@@ -798,6 +1012,71 @@ proc computeForwardedArgs(c: DepContext): seq[string] =
# replayed (`conf.icPreparsedConfig`); `commandIc` has already guaranteed it
# exists, else it bailed.
result.add "--icPreparsedConfig:" & c.config.icPreparsedConfig
# Everything else the user typed on the `nim ic` command line. The children
# replay the project's CONFIG FILES (ic_config.cfg.nif), never the driver's
# argv, so a switch that exists only there — `--opt:speed`, `--panics:on`,
# `--experimental:…`, `--passC:…` — silently did not reach them: `nim ic
# --opt:speed` produced a byte-identical debug binary. Forward the switches
# verbatim, minus the ones that MUST differ per child (the output/cache paths,
# the command itself, and IC's own per-rule switches, which each rule sets).
const notForwarded = [
"nimcache", "out", "o", "outdir", "usenimcache", "run", "r",
"incremental", "ic", "symbolfiles", "genbif",
"icproject", "icpreparsedconfig", "icconfigout", "icgroup",
"icbackendstage", "icbackendmodule", "ismainmodule",
"help", "h", "fullhelp", "version", "v", "advanced"]
for a in commandLineParams():
if a.len < 2 or a[0] != '-': continue
var i = 1
if i < a.len and a[i] == '-': inc i
var name = ""
while i < a.len and a[i] notin {':', '='}:
name.add a[i]
inc i
if normalize(name) notin notForwarded and a notin result:
result.add a
proc configSignatureFile(c: DepContext; forwardedArgs: seq[string]): string =
## nifmake decides staleness from file mtimes alone — it never looks at a
## rule's command line. So changing `-d:someDefine`, `--mm:` or `--threads:`
## between two `nim ic` runs re-generated the build file with the new switches
## but re-fired nothing: the user got a silently stale binary built with the
## OLD configuration. Reify the configuration as a FILE and make every rule
## that consumes it an input, so a config change moves an mtime like any edit.
## Written `OnlyIfChanged` so a genuine no-op run stays a no-op.
##
## Deliberately EXCLUDES the two per-build path switches (`--icproject:`,
## `--icPreparsedConfig:`): they name where this build lives, not what it
## produces, so including them made the signature differ between two caches
## holding byte-identical artifacts — which defeats prefilling a test's cache
## from a shared warm one (every rule would re-fire on the rewritten
## signature). The precompiled config still counts, by CONTENT: a `nim.cfg`
## edit changes the artifact, hence the hash, hence every rule.
result = getNimcacheDir(c.config).string / "ic_build_args.txt"
var content = ""
for p in c.config.searchPaths:
content.add "--path:" & p.string & "\n"
for a in forwardedArgs:
if a.startsWith("--icproject:") or a.startsWith("--icPreparsedConfig:"):
continue
content.add a & "\n"
if c.config.icPreparsedConfig.len > 0 and fileExists(c.config.icPreparsedConfig):
# Hash the precompiled config MINUS its `(nimcache "...")` entry — the one
# line in the artifact that records where this build's cache lives rather
# than what the config says. Everything else is genuinely config-derived, so
# two builds with the same `nim.cfg`/`config.nims` hash the same no matter
# which directory they run in.
var normalized = ""
try:
for line in lines(c.config.icPreparsedConfig):
if "(nimcache " in line: continue
normalized.add line
normalized.add '\n'
except IOError, OSError:
normalized = c.config.icPreparsedConfig
content.add "config:" & $secureHash(normalized) & "\n"
if not fileExists(result) or readFile(result) != content:
writeFile(result, content)
proc generateFrontendBuildFile(c: DepContext; forwardedArgs: seq[string]): string =
## Frontend build file: the nifler (parse) and `nim m` (sem) rules only. The
@@ -861,8 +1140,13 @@ proc generateFrontendBuildFile(c: DepContext; forwardedArgs: seq[string]): strin
b.addTree "output"
b.addStrLit parsed
b.endTree()
# The deps sidecar this command really produces is `<mod>.p.deps.nif`,
# not `<mod>.deps.nif` (which only the driver's `nifler deps` pre-scan
# writes). Declaring the latter made the rule permanently stale — a
# missing output is nifmake's strongest rebuild trigger — for every
# module the pre-scan does not also cover.
b.addTree "output"
b.addStrLit c.depsFile(pair)
b.addStrLit c.parsedDepsFile(pair)
b.endTree()
b.endTree()
@@ -878,6 +1162,7 @@ proc generateFrontendBuildFile(c: DepContext; forwardedArgs: seq[string]): strin
# a NIF for each. Only dependencies *outside* the component become build-graph
# inputs — intra-component edges are produced by this very rule and listing
# them would reintroduce the cycle nifmake just rejected.
let argsFile = configSignatureFile(c, forwardedArgs)
let sccs = computeSCCs(c)
var sccOf = newSeq[int](c.nodes.len)
for sccId, comp in sccs:
@@ -906,6 +1191,10 @@ proc generateFrontendBuildFile(c: DepContext; forwardedArgs: seq[string]): strin
# Input 0 (the project file passed to `nim m`): the representative's .nim.
b.withTree "input":
b.addStrLit repPair.nimFile
# The configuration this child is invoked with (see configSignatureFile).
b.addTree "input"
b.addStrLit argsFile
b.endTree()
# All parsed files of every member (nifler outputs this group consumes).
for m in members:
for f in c.nodes[m].files:
@@ -999,7 +1288,7 @@ proc backendCFile(c: DepContext; node: Node): string =
if node.id == 0: AbsoluteFile node.files[0].nimFile
else: AbsoluteFile node.files[0].modname
result = changeFileExt(completeCfilePath(c.config,
mangleModuleName(c.config, cfilename).AbsoluteFile), ".nim.c").string
mangleModuleName(c.config, cfilename).AbsoluteFile), icCFileExt(c.config)).string
proc computeLiveBackendNodes(c: DepContext): seq[bool] =
## Which nodes the backend must code-generate: the closure reachable from the
@@ -1031,6 +1320,76 @@ proc computeLiveBackendNodes(c: DepContext): seq[bool] =
let idx = c.processedModules.getOrDefault(c.toPair(p).modname, -1)
if idx >= 0: stack.add idx
proc intDefine(conf: ConfigRef; name: string; fallback: int): int =
## `-d:<name>:N` as an int, or `fallback` when unset or unparsable.
result = fallback
if isDefined(conf, name):
try: result = parseInt(conf.symbols[name])
except ValueError: result = fallback
proc backendBatchSize(conf: ConfigRef; liveCount: int): int =
## How many modules share one backend process. 1 is the historical per-module
## fan-out; larger batches amortise the process floor and the dependency
## closure load (measured on a 67-module program: 7.6 ms of process startup
## and ~10 ms of closure loading per child, against 3.5 ms of actual codegen).
##
## `-d:icBatchSize:N` pins it. The default is 1 — the plumbing is in place but
## the policy is not yet validated. `-d:icBatchSize:0` means "one batch per
## job", which is the shape a tuned default will take: enough batches to keep
## every core busy and no more, since a batch beyond that only buys
## amortisation at the price of parallelism.
if not isDefined(conf, "icBatchSize"): return 1
result = intDefine(conf, "icBatchSize", 1)
if result == 0:
let jobs =
if isDefined(conf, "icNoParallel"): 1
elif isDefined(conf, "icJobs"): max(1, intDefine(conf, "icJobs", 1))
elif conf.numberOfProcessors > 0: conf.numberOfProcessors
else: 1
result = (liveCount + jobs - 1) div jobs
result = max(1, result)
proc emitBatches(c: DepContext; live: seq[bool];
shared: seq[seq[int]]): seq[seq[int]] =
## emit's partition. Unlike `lower`/`cg` it takes the MAIN module too and, by
## default, puts every live node in one batch: emit owns no decisions, so
## there is nothing for a grouping to get wrong (see the rule that uses this).
## An explicit `-d:icBatchSize` reuses the shared partition instead, plus main,
## so the fan-out remains available to compare against.
if isDefined(c.config, "icBatchSize"):
result = shared
if live.len > 0 and live[0]: result.add @[0]
else:
var all: seq[int] = @[]
for i in 0 ..< c.nodes.len:
if live[i]: all.add i
result = if all.len > 0: @[all] else: @[]
proc backendBatches(c: DepContext; live: seq[bool]): seq[seq[int]] =
## Partition the live non-main nodes into batches of node indices. The main
## module is never in one: it loads the whole program, so batching it with
## anything defeats the memory bound the per-module split exists to give.
##
## Contiguous runs of `c.nodes`, which is import-traversal order, so a batch's
## members tend to share dependencies and its union closure stays close to one
## member's. A smarter partition (by closure overlap, or by the dirty set on an
## incremental build) belongs here and nowhere else — every stage already takes
## whatever grouping this returns.
var liveIdx: seq[int] = @[]
for i in 0 ..< c.nodes.len:
if live[i] and c.nodes[i].id != 0: liveIdx.add i
let size = backendBatchSize(c.config, liveIdx.len)
result = @[]
var i = 0
while i < liveIdx.len:
var batch: seq[int] = @[]
var j = i
while j < liveIdx.len and batch.len < size:
batch.add liveIdx[j]
inc j
result.add batch
i = j
proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string =
## Per-module backend build file. One `nim_nifc` command template (the actual
## stage/module switches ride in each rule's `(args …)`), then the stages of
@@ -1086,6 +1445,8 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
if fileExists(cnifFiles[i]) or fileExists(cFiles[i]): prunedStale = true
removeFile(cnifFiles[i])
removeFile(cFiles[i])
removeFile(cFiles[i] & ".stamp")
removeFile(cFiles[i] & BackendActionsExt)
# The merge decision is a pure function of the set of `.c.nif`s present; if we
# just removed an over-approximated module's artifacts, a decision computed
# while they were present is stale — it can name a now-absent module as a
@@ -1096,6 +1457,8 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
if prunedStale:
removeFile(mergeFile)
let argsFile = configSignatureFile(c, forwardedArgs)
var b = nifbuilder.open(result)
defer: b.close()
@@ -1145,16 +1508,39 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
# frontend writes `.s.nif`s content-stably, so an interface change to a
# dependency re-sems (and re-emits the `.s.nif` of) every transitive importer;
# a module whose own `.s.nif` is unchanged genuinely needs no re-lowering.
for i, node in c.nodes:
if not live[i]: continue
let batches = backendBatches(c, live)
template suffixList(batch: seq[int]): string =
var acc = ""
for k, idx in batch:
if k > 0: acc.add ","
acc.add c.nodes[idx].files[0].modname
acc
for batch in batches:
b.addTree "do"
b.addIdent "nim_nifc"
b.withTree "args":
b.addStrLit "--icBackendStage:lower"
b.addStrLit "--icBackendModule:" & node.files[0].modname
inputStr c.semmedFile(node.files[0])
outputStr tFiles[i]
b.addStrLit "--icBackendModules:" & suffixList(batch)
for idx in batch:
inputStr c.semmedFile(c.nodes[idx].files[0])
inputStr argsFile
for idx in batch:
outputStr tFiles[idx]
b.endTree()
# The main module is its own rule in every stage: it loads the whole program.
block:
let i = 0
if live[i]:
b.addTree "do"
b.addIdent "nim_nifc"
b.withTree "args":
b.addStrLit "--icBackendStage:lower"
b.addStrLit "--icBackendModules:" & c.nodes[i].files[0].modname
inputStr c.semmedFile(c.nodes[i].files[0])
inputStr argsFile
outputStr tFiles[i]
b.endTree()
# cg: one rule per module. Input is this module's OWN `.t.nif`. cg DOES read
# its dependencies' `.t.nif`s at runtime (loadDepClosure), but ordering is
@@ -1166,47 +1552,97 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
# emit-everywhere'd but does not own is dropped by `emit` regardless, so a
# stale copy here is harmless. The main module additionally depends on every
# other `.c.nif` (it reads their init/datInit metas to wire up NimMain).
for i, node in c.nodes:
if not live[i]: continue
for batch in batches:
b.addTree "do"
b.addIdent "nim_nifc"
b.withTree "args":
b.addStrLit "--icBackendStage:cg"
b.addStrLit "--icBackendModule:" & node.files[0].modname
inputStr tFiles[i]
if node.id == 0:
b.addStrLit "--icBackendModules:" & suffixList(batch)
for idx in batch:
inputStr tFiles[idx]
inputStr argsFile
for idx in batch:
outputStr cnifFiles[idx]
# The module's C compile/link directives (`{.passL.}` etc.), recorded so
# the `link` stage recovers them without loading the module graph. See
# `replayer.writeBackendActions`.
outputStr cFiles[idx] & BackendActionsExt
b.endTree()
block:
let i = 0
if live[i]:
b.addTree "do"
b.addIdent "nim_nifc"
b.withTree "args":
b.addStrLit "--icBackendStage:cg"
b.addStrLit "--icBackendModules:" & c.nodes[i].files[0].modname
inputStr tFiles[i]
inputStr argsFile
for j in 0 ..< c.nodes.len:
if c.nodes[j].id != 0 and live[j]:
inputStr cnifFiles[j]
outputStr cnifFiles[i]
b.endTree()
outputStr cnifFiles[i]
outputStr cFiles[i] & BackendActionsExt
b.endTree()
# merge: read every `.c.nif`, write the ownership/liveness decision.
# merge: read the live modules' `.c.nif`, write the ownership/liveness
# decision. The list is handed over as a FILE (`LiveModulesFile`) because the
# merge child is a separate process that never sees the build file: without it
# merge globbed `*.c.nif` off the nimcache and so silently absorbed artifacts
# belonging to some other program that shares the directory.
let liveFile = nimcache / LiveModulesFile
block:
var manifest = ""
for i in 0 ..< c.nodes.len:
if live[i]:
manifest.add cnifFiles[i]
manifest.add "\n"
# OnlyIfChanged: its mtime is a merge input, so rewriting it every run would
# re-fire merge (and, through the decision, every `emit`) on a no-op build.
if not fileExists(liveFile) or readFile(liveFile) != manifest:
writeFile(liveFile, manifest)
b.addTree "do"
b.addIdent "nim_nifc"
b.withTree "args":
b.addStrLit "--icBackendStage:merge"
for i in 0 ..< c.nodes.len:
if live[i]: inputStr cnifFiles[i]
inputStr liveFile
outputStr mergeFile
b.endTree()
# emit: render each module's `.c` from its `.c.nif` + the merge decision.
for i, node in c.nodes:
if not live[i]: continue
#
# ONE rule for everything, main included. emit is a pure function of a
# `.c.nif` and the merge decision — `renderCFromArtifact` filters text and
# touches no AST, and the stage loads no module graph at all — so batching it
# cannot change what it produces, and measurement agrees: 67 processes and one
# process give byte-identical `.c`, in 0.502 s versus 0.041 s. What that buys
# is not the cold build (where 0.5 s serial is ~0.05 s across cores) but the
# fire-all: every `emit` re-fires whenever `merge` rewrites the decision, which
# is every edit that reaches the backend. That now costs one process start.
#
# `-d:icBatchSize:N` still splits it, for A/B-ing against the fan-out.
for batch in emitBatches(c, live, batches):
b.addTree "do"
b.addIdent "nim_nifc"
b.withTree "args":
b.addStrLit "--icBackendStage:emit"
b.addStrLit "--icBackendModule:" & node.files[0].modname
# Inputs: this module's OWN `.c.nif` and the global merge decision. emit also
# loads `.t.nif`s at runtime (getCFile/type resolution), but those are depth 1
# and emit is past the merge barrier, so they always exist — no need to list
# them. (emit still re-fires for every module whenever `merge` rewrites the
# decision file; making that incremental is a separate concern.)
inputStr cnifFiles[i]
b.addStrLit "--icBackendModules:" & suffixList(batch)
# Inputs: each member's OWN `.c.nif` and the global merge decision. emit
# reads nothing else — it derives its output paths rather than loading a
# module graph. (It still re-fires for every module whenever `merge` rewrites
# the decision file; making that incremental is a separate concern — though
# batching is what makes the re-fire cheap.)
for idx in batch:
inputStr cnifFiles[idx]
inputStr mergeFile
outputStr cFiles[i]
for idx in batch:
outputStr cFiles[idx]
# The freshness proof for this rule; see nifbackend.generateEmitStage. The
# `.c` alone cannot serve: it is written OnlyIfChanged, so a rule that ran
# and produced identical bytes looks exactly like a rule that never ran.
outputStr cFiles[idx] & ".stamp"
b.endTree()
# link: compile + link every emitted `.c` in one process.
@@ -1220,12 +1656,62 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
# path splits back into outDir+outFile in the child).
b.addStrLit "--out:" & exeFile
for i in 0 ..< c.nodes.len:
if live[i]: inputStr cFiles[i]
if live[i]:
inputStr cFiles[i]
inputStr cFiles[i] & BackendActionsExt
inputStr argsFile
outputStr exeFile
b.endTree()
b.endTree() # stmts
proc deriveFromSemDeps(c: var DepContext): bool =
## Fold every already-compiled module's `.s.deps` sidecar (its REAL post-sem
## imports, macro-generated ones included) back into the graph. Returns true
## if anything new was added.
##
## Run BEFORE the first nifmake pass as well as after a failure. The static
## scanner cannot see `parseStmt("import dyn")`, so on the run that first hits
## it the frontend fails, this recovers the node, and the retry succeeds. But
## the graph is rebuilt from scratch on every `nim ic`, so on the NEXT run the
## frontend succeeds on round one — with `dyn` absent from the graph again,
## hence with no nifler/`nim m` rule of its own and no edge into its importer.
## Editing `dyn.nim` then changed nothing at all: the build silently reused the
## `.s.bif` from the run that discovered it. Seeding from the sidecars makes
## the discovery stick across runs.
##
## The edges are recorded SPECULATIVELY: a sidecar says what the module
## imported the last time it was semmed, which is a statement about the past.
## Flip a `when`, or delete an `import`, and a module that is no longer reached
## would otherwise linger in the graph forever (and fail to build, if what it
## imports is gone). Marking the edge speculative lets `pruneDeadSpeculative`
## drop such a leftover, while a genuinely-needed macro import — which compiles
## fine — stays.
result = false
inc c.speculating
defer: dec c.speculating
let n0 = c.nodes.len # snapshot: new nodes are traversed as they're added
for ni in 0 ..< n0:
for p in readSemDeps(c, c.nodes[ni].files[0]):
let pair = c.toPair(p)
var idx = c.processedModules.getOrDefault(pair.modname, -1)
if idx == -1:
if not fileExists(pair.nimFile): continue
let newNode = Node(files: @[pair], id: c.nodes.len)
if c.systemNodeId >= 0:
newNode.deps.add c.systemNodeId
if getsImplicitImports(c, pair.nimFile):
for impId in c.implicitNodeIds:
if impId != newNode.id: newNode.deps.add impId
c.processedModules[pair.modname] = newNode.id
c.nodes.add newNode
idx = newNode.id
traverseDeps(c, pair, newNode)
result = true
if idx != ni and idx notin c.nodes[ni].deps:
addDepEdge(c, c.nodes[ni], idx)
result = true
proc commandIc*(conf: ConfigRef; frontendOnly = false) =
## Main entry point for `nim ic`. With `frontendOnly` (used by `nim track` for
## IDE queries) it runs only Phase 1 — the incremental nifler + `nim m`
@@ -1323,6 +1809,17 @@ proc commandIc*(conf: ConfigRef; frontendOnly = false) =
# Process dependencies
traverseDeps(c, rootPair, rootNode)
# Re-apply what earlier runs discovered post-sem (macro-generated imports),
# so those modules keep their rules on a warm build instead of vanishing from
# the graph until the next failure. No-op on a cold cache. Runs BEFORE the
# prune so a sidecar entry that has since gone stale is prunable too.
discard deriveFromSemDeps(c)
# Modules that only a `when` the scanner cannot decide pulls in, and that
# import something not installed, are dead in this configuration; scheduling
# them would fail the build over code the classic compiler never reads.
pruneDeadSpeculative(c)
# Discovery via `.s.deps`: imports GENERATED by macros (chronicles builds
# `import chronicles/textlines` via parseStmt from the chronicles_sinks
# define) are invisible to the static scanner. Each `nim m` records the
@@ -1393,28 +1890,20 @@ proc commandIc*(conf: ConfigRef; frontendOnly = false) =
var discovered = false
inc rounds
if rounds <= 20:
let n0 = c.nodes.len # snapshot: new nodes are traversed as they're added
for ni in 0 ..< n0:
for p in readSemDeps(c, c.nodes[ni].files[0]):
let pair = c.toPair(p)
var idx = c.processedModules.getOrDefault(pair.modname, -1)
if idx == -1:
let newNode = Node(files: @[pair], id: c.nodes.len)
if c.systemNodeId >= 0:
newNode.deps.add c.systemNodeId
if getsImplicitImports(c, pair.nimFile):
for impId in c.implicitNodeIds:
if impId != newNode.id: newNode.deps.add impId
c.processedModules[pair.modname] = newNode.id
c.nodes.add newNode
idx = newNode.id
traverseDeps(c, pair, newNode)
discovered = true
if idx != ni and idx notin c.nodes[ni].deps:
c.nodes[ni].deps.add idx
discovered = true
discovered = deriveFromSemDeps(c)
if not discovered:
rawMessage(conf, errGenerated, "nifmake failed with exit code: " & $exitCode)
# The children have already printed the real diagnostics. Adding an
# `Error:` line of our own here made a build-system status the LAST error
# in the stream, hiding the compiler's own message from anything that
# reads the final error (testament's `errormsg:`, editors, CI log
# scrapers) — every `reject`-style test under `nim ic` reported
# "nifmake failed with exit code: 1" instead of what the compiler said.
# The non-zero exit is what signals failure; this line is context.
rawMessage(conf, hintExecuting,
"nifmake reported failures (exit code " & $exitCode & ")")
# Fail the run without printing an `Error:` of our own (see above): the
# exit code is derived from `errorCounter`.
inc conf.errorCounter
break
# Phase 2 — backend (whole-program `nim nifc`), run once over the now-final
@@ -1429,6 +1918,8 @@ proc commandIc*(conf: ConfigRef; frontendOnly = false) =
rawMessage(conf, hintExecuting, cmd)
let exitCode = execShellCmd(cmd)
if exitCode != 0:
rawMessage(conf, errGenerated, "nifmake (backend) failed with exit code: " & $exitCode)
rawMessage(conf, hintExecuting,
"nifmake reported backend failures (exit code " & $exitCode & ")")
inc conf.errorCounter
else:
rawMessage(conf, errGenerated, "nim ic not available in bootstrap build")

View File

@@ -454,7 +454,7 @@ proc gen(c: var Con; n: PNode) =
of nkPragmaBlock: gen(c, n.lastSon)
of nkDiscardStmt, nkObjDownConv, nkObjUpConv, nkStringToCString, nkCStringToString:
gen(c, n[0])
of nkConv, nkExprColonExpr, nkExprEqExpr, nkCast, PathKinds1:
of nkConv, nkExprColonExpr, nkExprEqExpr, PathKinds1:
gen(c, n[1])
of nkVarSection, nkLetSection: genVarSection(c, n)
of nkDefer: raiseAssert "dfa construction pass requires the elimination of 'defer'"

View File

@@ -14,7 +14,7 @@ proc genEnumToStrProc*(t: PType; info: TLineInfo; g: ModuleGraph; idgen: IdGener
let res = newSym(skResult, getIdent(g.cache, "result"), idgen, result, info)
res.typ = getSysType(g, info, tyString)
result.typ = newType(tyProc, idgen, t.owner)
result.typ = newType(tyProc, idgen, result)
result.typ.n = newNodeI(nkFormalParams, info)
rawAddSon(result.typ, res.typ)
result.typ.n.add newNodeI(nkEffectList, info)

View File

@@ -14,11 +14,77 @@
import ".." / [ast, modulegraphs, trees, extccomp, btrees,
msgs, lineinfos, pathutils, options, cgmeth]
import std/tables
import std/[tables, os, strutils, syncio]
when defined(nimPreviewSlimSystem):
import std/assertions
const BackendActionsExt* = ".cflags"
## Sidecar written by a module's `cg` stage next to its `.c`, carrying the C
## compile/link directives that module's `{.passL.}`/`{.compile.}`/… pragmas
## recorded. See `writeBackendActions`.
proc writeBackendActions*(g: ModuleGraph; module: PSym; list: PNode;
outfile: string) =
## Serialize the backend-relevant replay actions of ONE module to `outfile`,
## one tab-separated action per line.
##
## The `link` stage used to recover these by loading the whole import closure
## as `PrecompiledModule`s and re-running `replayBackendActions` over each —
## a 3.7s whole-program graph load, per link, purely to recover a handful of
## strings and the modules' `.c` paths. The producing `cg` process already has
## them in hand, so it writes them down instead and `link` reads them back
## (`applyBackendActions`). Written unconditionally, even when empty: it is a
## declared nifmake output of the `cg` rule, and a missing output re-fires the
## rule for ever.
##
## `localpassc` needs the module's own source path, which only the writer can
## resolve, so it is baked in here as a third field.
var content = ""
if list != nil:
for n in list:
if n.kind == nkReplayAction and n.len >= 2 and
n[0].kind == nkStrLit and n[1].kind == nkStrLit:
case n[0].strVal
of "compile":
if n.len == 4 and n[2].kind == nkStrLit and n[3].kind == nkStrLit:
content.add "compile\t" & n[1].strVal & "\t" & n[2].strVal & "\t" &
n[3].strVal & "\n"
of "link", "passl", "passc", "cppdefine":
content.add n[0].strVal & "\t" & n[1].strVal & "\n"
of "localpassc":
content.add "localpassc\t" & n[1].strVal & "\t" &
toFullPathConsiderDirty(g.config, module.info.fileIndex).string & "\n"
else: discard
writeFile(outfile, content)
proc applyBackendActions*(g: ModuleGraph; infile: string) =
## Apply one module's recorded C directives (see `writeBackendActions`). The
## `link` stage's replacement for loading that module and replaying its AST.
if not fileExists(infile): return
for line in lines(infile):
if line.len == 0: continue
let f = line.split('\t')
case f[0]
of "compile":
if f.len == 4:
let cname = AbsoluteFile f[1]
var cf = Cfile(nimname: splitFile(cname).name, cname: cname,
obj: AbsoluteFile f[2],
flags: {CfileFlag.External}, customArgs: f[3])
extccomp.addExternalFileToCompile(g.config, cf)
of "link":
if f.len == 2: extccomp.addExternalFileToLink(g.config, AbsoluteFile f[1])
of "passl":
if f.len == 2: extccomp.addLinkOption(g.config, f[1])
of "passc":
if f.len == 2: extccomp.addCompileOption(g.config, f[1])
of "localpassc":
if f.len == 3: extccomp.addLocalCompileOption(g.config, f[1], AbsoluteFile f[2])
of "cppdefine":
if f.len == 2: options.cppDefine(g.config, f[1])
else: discard
proc replayStateChanges*(module: PSym; g: ModuleGraph; list: PNode) =
## `list` is an `nkStmtList` of `nkReplayAction` nodes (macro-cache puts/incs/
## adds/incls and a few pragmas) recorded for `module`. Under the NIF backend a

View File

@@ -32,7 +32,7 @@
## would misresolve.
import options, commands, lineinfos, pathutils, msgs
import std/[algorithm, os, sets, osproc, times, streams, syncio]
import std/[algorithm, os, sets, osproc, times, streams, syncio, strutils]
import "../dist/nimony/src/lib" / [nifbuilder, nifcoreparse]
const
@@ -269,11 +269,26 @@ proc ensureIcConfig*(conf: ConfigRef) =
# verbatim: all `-`-prefixed switches first (in encounter order), then the
# non-switch project token(s). The producer re-reads `nim.cfg` itself.
var pargs = @["icconfig", "--icConfigOut:" & outPath]
# The command token is dropped below, so `nim cpp --ic:on` would hand the
# producer a C-backend config: name the backend explicitly. (`nim ic
# --backend:cpp` already carries the switch; the duplicate is harmless.)
if conf.backend != backendInvalid:
pargs.add "--backend:" & $conf.backend
var rest: seq[string] = @[]
var droppedCmd = false
for a in commandLineParams():
if a.len == 0: continue
if a[0] == '-':
# `--run`/`-r` must not reach the producer: it only serialises the
# resolved config, has no output binary, and `nim.nim`'s run step asserts
# on the empty `outFile` (`nim cpp --ic:on -r foo.nim`).
var name = ""
var i = 1
if i < a.len and a[i] == '-': inc i
while i < a.len and a[i] notin {':', '='}:
name.add a[i]
inc i
if normalize(name) in ["r", "run"]: continue
pargs.add a
elif not droppedCmd:
droppedCmd = true # drop the original command token (`ic`/`track`)

114
compiler/icprof.nim Normal file
View File

@@ -0,0 +1,114 @@
#
#
# The Nim Compiler
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Opt-in instrumentation for the IC backend, enabled with `-d:icBNodeProf`.
## Off, every template below is `discard` and nothing is linked in.
##
## It lives in its own module with NO compiler imports so that any stage can
## use it without creating a cycle — `bnode` needs it for the accessors,
## `nifbackend` for the stage phases, `cgen` for what happens per routine.
##
## Each backend process appends ONE line to `$NIM_IC_BNODE_PROF` at exit (or to
## stderr when that is unset), because a `--ic:on` build fans out a process per
## module per stage and interleaved writes would tear. Use `-d:icNoParallel`
## when the numbers need to be attributable to a particular module.
##
## Counts are for volume, timings for cost, and the two answer different
## questions: the accessors turned out to be 700k calls worth 8ms, while `info`
## was 259k calls worth 1.36s. Neither number alone would have found that.
when defined(icBNodeProf):
import std / [envvars, exitprocs, syncio, monotimes]
from std / times import inNanoseconds
type
ProfSlot* = enum
pKind, pTagKindHit, pTagKindMiss, pAstChildren, pSkip, pSon, pLen,
pLastSon, pIterYield, pSym, pTyp, pTypTagLit, pOrigin, pNilType,
pGenBodyCalls, pInfo, pIfaceExported, pIfaceHidden, pIfaceModules,
pTopNodes, pExportSyms, pPeekKind, pPeekFallback, pPeekLoaded,
pTopToolingSkip
TimeSlot* = enum
tLoadClosure, tModuleId, tBifLoad, tPosIndex, tTopLevel, tInterfTables,
tTransform, tHandOff, tGenBody, tAnalyses,
tSym, tTyp, tInfo, tOrigin, tExportBranch, tResolveSym, tEnumFields,
# Coarse phases, added to find where a backend process spends the time
# that none of the slots above account for. `tStage` is the whole stage
# body, so `Process - tStage` is everything before it: exec, the Nim
# runtime, config replay, `registerNifSuffix`/graph setup.
tStage,
tLowerOwned, tLowerHooks, tLowerWrite,
tCgGen, tCgInit, tCgFinish, tCgWrite,
tMergeStage, tEmitRender, tLinkStage,
# `nim m` (the frontend): the sem pass as a whole, and writing the module's
# `.s.bif`. `Stage - WriteNif - <the loading slots>` is then sem proper.
tWriteNif,
# `processTopLevel`'s branches: which part of a module HEADER costs what.
tTopReplay, tTopLogOps, tTopOffers, tTopStmts
let procStart = getMonoTime()
## Set when this module initialises, i.e. essentially at process start, so
## the dump can report total process wall time and the startup share can be
## derived as `Process - Stage`.
var profStageName* = "frontend"
## Which invocation this is: the backend stage name, or "frontend" for a
## `nim m` process, which arms the profiler through ast2nif but never enters
## a backend stage. Without it the `Process - Stage` startup figure is
## meaningless — 204 frontend processes' whole runtime lands in it.
var profCounts: array[ProfSlot, int]
var profNanos: array[TimeSlot, int64]
var profStart: array[TimeSlot, MonoTime]
var profArmed = false
proc profDump() =
var line = "BNODEPROF stage=" & profStageName
for s in ProfSlot: line.add " " & ($s)[1..^1] & "=" & $profCounts[s]
for s in TimeSlot: line.add " " & ($s)[1..^1] & "ms=" & $(profNanos[s] div 1_000_000)
line.add " Processms=" & $((getMonoTime() - procStart).inNanoseconds div 1_000_000)
let f = getEnv("NIM_IC_BNODE_PROF")
if f.len > 0:
let h = open(f, fmAppend)
h.writeLine line
h.close()
else:
stderr.writeLine line
template armProf() =
if not profArmed:
profArmed = true
addExitProc profDump
template prof*(s: ProfSlot; n = 1) =
armProf()
inc profCounts[s], n
template icProfStart*(s: TimeSlot) =
armProf()
profStart[s] = getMonoTime()
template icProfStop*(s: TimeSlot) =
profNanos[s] += (getMonoTime() - profStart[s]).inNanoseconds
template timed*(s: TimeSlot; body: untyped) =
## Leaf timing. NOT re-entrant, and the phase slots are not disjoint —
## `tTransform` contains body materialization, `tTyp` reaches `tSym`. Read
## them as nested, not additive.
##
## Arms the dump like `prof`/`icProfStart` do. It did not, and so a process
## whose ONLY instrumentation is a `timed` never reported at all: the
## `merge`, `emit` and `link` stages were silently absent from every profile.
armProf()
let t0 = getMonoTime()
body
profNanos[s] += (getMonoTime() - t0).inNanoseconds
else:
template prof*(s: untyped; n = 1) = discard
template icProfStart*(s: untyped) = discard
template icProfStop*(s: untyped) = discard
template timed*(s: untyped; body: untyped) = body

View File

@@ -423,6 +423,20 @@ proc genDefaultCall(t: PType; c: Con; info: TLineInfo): PNode =
result.add(newSymNode(createMagic(c.graph, c.idgen, "default", mDefault)))
result.typ = t
proc stabilizeBracketIndex(n: PNode; c: var Con; body: var PNode): PNode =
## Evaluate a side-effecting index once and return the stable access.
doAssert n.kind == nkBracketExpr and not isAtom(n[1])
let temp = newSym(skLet, getIdent(c.graph.cache, "bracketTmp"), c.idgen,
c.owner, n[1].info)
temp.typ = n[1].typ
let tempAsNode = newSymNode(temp)
body.add newTree(nkLetSection, n[1].info,
newTree(nkIdentDefs, tempAsNode,
newNodeI(nkEmpty, tempAsNode.info), n[1]))
result = copyNode(n)
result.add n[0]
result.add tempAsNode
proc destructiveMoveVar(n: PNode; c: var Con; s: var Scope): PNode =
# generate: (let tmp = v; reset(v); tmp)
if (not hasDestructor(c, n.typ)) and c.inEnsureMove == 0:
@@ -434,6 +448,10 @@ proc destructiveMoveVar(n: PNode; c: var Con; s: var Scope): PNode =
else:
result = newNodeIT(nkStmtListExpr, n.info, n.typ)
var n = n
if n.kind == nkBracketExpr and not isAtom(n[1]):
n = stabilizeBracketIndex(n, c, result)
var temp = newSym(skLet, getIdent(c.graph.cache, "blitTmp"), c.idgen, c.owner, n.info)
temp.typ = n.typ
var v = newNodeI(nkLetSection, n.info)
@@ -1119,6 +1137,11 @@ proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode; tmpFlags = {sfSing
result[i] = n[i]
of nkGotoState, nkState, nkAsmStmt:
result = n
of nkReplayAction:
# A `.rod`/NIF replay record. It only ever appears in a NIF-loaded
# module's TOP-LEVEL statements (the loader prepends the `(replay ...)`
# entries there); cgen discards it, so pass it through untouched.
result = n
else:
result = nil
internalError(c.graph.config, n.info, "cannot inject destructors to node kind: " & $n.kind)
@@ -1150,24 +1173,11 @@ proc sameLocation*(a, b: PNode): bool =
else: false
proc genFieldAccessSideEffects(c: var Con; s: var Scope; dest, ri: PNode; flags: set[MoveOrCopyFlag] = {}): PNode =
# with side effects
var temp = newSym(skLet, getIdent(c.graph.cache, "bracketTmp"), c.idgen, c.owner, ri[1].info)
temp.typ = ri[1].typ
var v = newNodeI(nkLetSection, ri[1].info)
let tempAsNode = newSymNode(temp)
var vpart = newNodeI(nkIdentDefs, tempAsNode.info, 3)
vpart[0] = tempAsNode
vpart[1] = newNodeI(nkEmpty, tempAsNode.info)
vpart[2] = ri[1]
v.add(vpart)
var newAccess = copyNode(ri)
newAccess.add ri[0]
newAccess.add tempAsNode
var snk = c.genSink(s, dest, newAccess, flags)
result = newTree(nkStmtList, v, snk, c.genWasMoved(newAccess))
result = newNodeI(nkStmtList, ri.info)
let newAccess = stabilizeBracketIndex(ri, c, result)
let snk = c.genSink(s, dest, newAccess, flags)
result.add snk
result.add c.genWasMoved(newAccess)
proc ownsData(c: var Con; s: var Scope; orig: PNode; flags: set[MoveOrCopyFlag]): PNode =
var n = orig

View File

@@ -1450,6 +1450,20 @@ proc genCheckedFieldOp(p: PProc, n: PNode, addrTyp: PType, r: var TCompRes) =
r.res = "$1.$2" % [tmp, field.loc.snippet]
r.kind = resExpr
proc isVarOpenArrayParam(n: PNode): bool =
## True if `n` resolves to a `var openArray` parameter. The JS backend
## represents such parameters as a `{base, off, len}` slice view so that
## writes through a `toOpenArray` view reach the caller's storage (bug #15952).
var it = n
while true:
case it.kind
of nkHiddenDeref, nkDerefExpr, nkHiddenAddr, nkAddr: it = it[0]
of nkHiddenStdConv, nkConv, nkObjDownConv, nkObjUpConv: it = it[1]
else: break
result = it.kind == nkSym and it.sym.kind == skParam and
it.sym.typ != nil and it.sym.typ.kind == tyVar and
it.sym.typ.len > 0 and it.sym.typ[0].kind == tyOpenArray
proc genArrayAddr(p: PProc, n: PNode, r: var TCompRes) =
var
a, b: TCompRes = default(TCompRes)
@@ -1458,6 +1472,19 @@ proc genArrayAddr(p: PProc, n: PNode, r: var TCompRes) =
let m = if n.kind == nkHiddenAddr: n[0] else: n
gen(p, m[0], a)
gen(p, m[1], b)
if isVarOpenArrayParam(m[0]):
# `var openArray` param is a `{base, off, len}` view; index the base with
# the offset applied. `m[0]` is a plain param name, safe to reference
# repeatedly (no side effects, so no temp needed).
let pn = a.rdLoc
r.address = "($1).base" % [pn]
if optBoundsCheck in p.options:
useMagic(p, "chckIndx")
r.res = "($1).off + chckIndx($2, 0, ($1).len - 1)" % [pn, b.rdLoc]
else:
r.res = "($1).off + ($2)" % [pn, b.rdLoc]
r.kind = resExpr
return
#internalAssert p.config, a.typ != etyBaseIndex and b.typ != etyBaseIndex
let (x, tmp) = maybeMakeTemp(p, m[0], a)
r.address = x
@@ -1726,8 +1753,47 @@ proc genArgNoParam(p: PProc, n: PNode, r: var TCompRes) =
else:
r.res.add(a.res)
proc genVarOpenArrayArg(p: PProc, n: PNode, r: var TCompRes) =
## Emit a `{base, off, len}` slice view for an argument to a `var openArray`
## parameter (bug #15952). The view always aliases the base storage, so writes
## through the callee's `openArray` reach the caller's array/seq/typed array.
var b, lo, hi, v: TCompRes = default(TCompRes)
# the argument reaches codegen as `addr(toOpenArray(x, lo, hi))` (possibly
# under conversions); unwrap to the actual `toOpenArray` call.
var sl = n
while true:
case sl.kind
of nkHiddenAddr, nkAddr, nkHiddenDeref, nkDerefExpr: sl = sl[0]
of nkHiddenStdConv, nkConv, nkObjDownConv, nkObjUpConv: sl = sl[1]
else: break
if sl.kind in nkCallKinds and getMagic(sl) == mSlice:
gen(p, sl[1], b)
gen(p, sl[2], lo)
gen(p, sl[3], hi)
if isVarOpenArrayParam(sl[1]):
# slicing a `var openArray` view: rebase onto the same underlying storage
r.res = "{base: ($1).base, off: ($1).off + $2, len: $3 - $2 + 1}" % [
b.rdLoc, lo.rdLoc, hi.rdLoc]
else:
r.res = "{base: $1, off: $2, len: $3 - $2 + 1}" % [
b.rdLoc, lo.rdLoc, hi.rdLoc]
elif isVarOpenArrayParam(sl):
# already a view from another `var openArray` param: forward it unchanged
gen(p, sl, b)
r.res = b.rdLoc
else:
# a whole array/seq/typed-array value: wrap with a zero offset
gen(p, n, v)
r.res = "{base: $1, off: 0, len: ($1).length}" % [v.rdLoc]
r.kind = resExpr
proc genArg(p: PProc, n: PNode, param: PSym, r: var TCompRes; emitted: ptr int = nil) =
var a: TCompRes = default(TCompRes)
if param.typ != nil and param.typ.kind == tyVar and param.typ[0].kind == tyOpenArray:
# `var openArray` params are passed as a `{base, off, len}` slice view.
genVarOpenArrayArg(p, n, a)
r.res.add(a.rdLoc)
return
gen(p, n, a)
if skipTypes(param.typ, abstractVar).kind in {tyOpenArray, tyVarargs} and
a.typ == etyBaseIndex:
@@ -1737,6 +1803,13 @@ proc genArg(p: PProc, n: PNode, param: PSym, r: var TCompRes; emitted: ptr int =
r.res.add(", ")
r.res.add(a.res)
if emitted != nil: inc emitted[]
elif skipTypes(param.typ, abstractVar).kind == tyOpenArray and
isVarOpenArrayParam(n):
# a `var openArray` view passed to a read-only `openArray` param: materialize
# a snapshot so the callee sees a plain array.
var w: TCompRes = default(TCompRes)
gen(p, n, w)
r.res.add("(($1).base).slice(($1).off, ($1).off + ($1).len)" % [w.rdLoc])
elif n.typ.kind in {tyVar, tyPtr, tyRef, tyLent, tyOwned} and
n.kind in nkCallKinds and mapType(param.typ) == etyBaseIndex:
# this fixes bug #5608:
@@ -2371,13 +2444,21 @@ proc genMagic(p: PProc, n: PNode, r: var TCompRes) =
useMagic(p, "nimCopy")
r.res = "nimCopy(null, $1, $2)" % [x.rdLoc, genTypeInfo(p, n.typ)]
of mOpenArrayToSeq:
genCall(p, n, r)
if isVarOpenArrayParam(n[1]):
var x: TCompRes = default(TCompRes)
gen(p, n[1], x)
r.res = "(($1).base).slice(($1).off, ($1).off + ($1).len)" % [x.rdLoc]
r.kind = resExpr
else:
genCall(p, n, r)
of mDestroy, mTrace: discard "ignore calls to the default destructor"
of mOrd: genOrd(p, n, r)
of mLengthStr, mLengthSeq, mLengthOpenArray, mLengthArray:
var x: TCompRes = default(TCompRes)
gen(p, n[1], x)
if skipTypes(n[1].typ, abstractInst).kind == tyCstring:
if isVarOpenArrayParam(n[1]):
r.res = "($1).len" % [x.rdLoc]
elif skipTypes(n[1].typ, abstractInst).kind == tyCstring:
let (a, tmp) = maybeMakeTemp(p, n[1], x)
r.res = "(($1) == null ? 0 : ($2).length)" % [a, tmp]
else:
@@ -2386,7 +2467,9 @@ proc genMagic(p: PProc, n: PNode, r: var TCompRes) =
of mHigh:
var x: TCompRes = default(TCompRes)
gen(p, n[1], x)
if skipTypes(n[1].typ, abstractInst).kind == tyCstring:
if isVarOpenArrayParam(n[1]):
r.res = "($1).len - 1" % [x.rdLoc]
elif skipTypes(n[1].typ, abstractInst).kind == tyCstring:
let (a, tmp) = maybeMakeTemp(p, n[1], x)
r.res = "(($1) == null ? -1 : ($2).length - 1)" % [a, tmp]
else:
@@ -2469,11 +2552,24 @@ proc genMagic(p: PProc, n: PNode, r: var TCompRes) =
genCall(p, n, r)
of mSlice:
# arr.slice([begin[, end]]): 'end' is exclusive
# Fixed homogeneous numeric arrays lower to JS typed arrays; `slice`
# copies, which silently breaks `var openArray` write-through (bug #15952).
# `subarray` returns a live shared-buffer view with the same
# exclusive-end signature, so use it there; keep `slice` for seqs/strings.
var x, y, z: TCompRes = default(TCompRes)
gen(p, n[1], x)
gen(p, n[2], y)
gen(p, n[3], z)
r.res = "($1.slice($2, $3 + 1))" % [x.rdLoc, y.rdLoc, z.rdLoc]
if isVarOpenArrayParam(n[1]):
# re-slicing a `var openArray` view: materialize from the view's base/offset
r.res = "(($1).base).slice(($1).off + $2, ($1).off + $3 + 1)" % [
x.rdLoc, y.rdLoc, z.rdLoc]
else:
let baseTy = skipTypes(n[1].typ, abstractVarRange + {tyLent})
if baseTy.kind == tyArray and arrayTypeForElemType(p.config, elemType(baseTy)).len > 0:
r.res = "($1.subarray($2, $3 + 1))" % [x.rdLoc, y.rdLoc, z.rdLoc]
else:
r.res = "($1.slice($2, $3 + 1))" % [x.rdLoc, y.rdLoc, z.rdLoc]
r.kind = resExpr
of mMove:
genMove(p, n, r)

View File

@@ -675,6 +675,17 @@ proc rawClosureCreation(owner: PSym;
if up != nil and upField.typ.skipTypes({tyOwned, tyRef, tyPtr}) == up.typ.skipTypes({tyOwned, tyRef, tyPtr}):
result.add(newAsgnStmt(rawIndirectAccess(env, upField, env.info),
up, env.info))
# That assignment stores a real `ref`, so `injectDestructorCalls` has to
# find the up-field type's ops — otherwise it stays a raw pointer store,
# the enclosing env'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. Whole-program cgen never
# noticed: some LATER lifting pass creates this very ref type's ops, and it
# runs before any routine's destructor 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`).
if up.typ != nil and up.typ.kind == tyRef and up.typ.elementType != nil:
createTypeBoundOpsLL(d.graph, up.typ, env.info, d.idgen, owner)
#elif oldenv != nil and oldenv.typ == upField.typ:
# result.add(newAsgnStmt(rawIndirectAccess(env, upField, env.info),
# oldenv, env.info))
@@ -732,6 +743,10 @@ proc closureCreationForIter(owner: PSym, iter: PNode;
if u != nil and u.typ.skipTypes({tyOwned, tyRef, tyPtr}) == expectedUpTyp:
result.add(newAsgnStmt(rawIndirectAccess(vnode, upField, iter.info),
u, iter.info))
# See the identical call in `rawClosureCreation`: the up-field's ops must
# exist by the time this assignment is destructor-injected.
if u.typ != nil and u.typ.kind == tyRef and u.typ.elementType != nil:
createTypeBoundOpsLL(d.graph, u.typ, iter.info, d.idgen, owner)
else:
localError(d.graph.config, iter.info, "internal error: cannot create up reference for iter")
result.add makeClosure(d.graph, d.idgen, iter.sym, vnode, iter.info)

View File

@@ -82,7 +82,7 @@ proc lookup(typeMap: ref LayeredIdTableObj, key: ItemId): PType =
template lookup*(typeMap: ref LayeredIdTableObj, key: PType): PType =
## recursively looks up binding of `key` in all parent layers
lookup(typeMap, key.itemId)
lookup(typeMap, key.bindingId)
when not useRef:
proc lookup(typeMap: LayeredIdTableObj, key: ItemId): PType {.inline.} =
@@ -91,11 +91,11 @@ when not useRef:
result = lookup(typeMap.nextLayer, key)
template lookup*(typeMap: LayeredIdTableObj, key: PType): PType =
lookup(typeMap, key.itemId)
lookup(typeMap, key.bindingId)
proc put(typeMap: var LayeredIdTable, key: ItemId, value: PType) {.inline.} =
typeMap.topLayer[key] = value
template put*(typeMap: var LayeredIdTable, key, value: PType) =
## binds `key` to `value` only in current layer
put(typeMap, key.itemId, value)
put(typeMap, key.bindingId, value)

View File

@@ -718,7 +718,7 @@ proc useSeqOrStrOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
when defined(icDbg):
if t.destructor == nil:
echo "MISSING destructor: ", typeToString(t), " kind=", t.kind,
" itemId=", t.itemId, " uniqueId=", t.uniqueId, " state=", t.state,
" itemId=", t.itemId, " bindingId=", t.bindingId, " state=", t.state,
" owner=", (if t.owner != nil: t.owner.name.s else: "nil")
doAssert t.destructor != nil
body.add destructorCall(c, t.destructor, x)
@@ -1233,7 +1233,7 @@ proc symDupPrototype(g: ModuleGraph; typ: PType; owner: PSym; kind: TTypeAttache
res.typ = typ
src.typ = typ
result.typ = newType(tyProc, idgen, owner)
result.typ = newType(tyProc, idgen, result)
result.typ.n = newNodeI(nkFormalParams, info)
rawAddSon(result.typ, res.typ)
result.typ.n.add newNodeI(nkEffectList, info)
@@ -1279,7 +1279,8 @@ proc symPrototype(g: ModuleGraph; typ: PType; owner: PSym; kind: TTypeAttachedOp
else:
src.typ = typ
result.typ = newProcType(info, idgen, owner)
# the hook OWNS its signature, like any routine sem'd from source
result.typ = newProcType(info, idgen, result)
result.typ.addParam dest
if kind notin {attachedDestructor, attachedWasMoved}:
result.typ.addParam src

View File

@@ -29,7 +29,8 @@ when defined(nimPreviewSlimSystem):
import ../dist/checksums/src/checksums/sha1
import pipelines
from icconfig import produceIcConfig
import icprof
from icconfig import produceIcConfig, ensureIcConfig
when not defined(nimKochBootstrap):
import nifbackend
@@ -269,6 +270,28 @@ proc mainCommand*(graph: ModuleGraph) =
proc compileToBackend() =
customizeForBackend(conf.backend)
if isIcDriver(conf):
# `nim c --ic:on` / `nim cpp --ic:on`: same driver as `nim ic`, entered
# through the ordinary compile command so every backend switch the user
# already knows keeps working (`nim cpp`, `--exceptions:`, `-d:`, ...).
# `customizeForBackend` above has already defined the backend symbol and
# picked the exception model, which is exactly what the per-module
# children must inherit — `computeForwardedArgs` forwards both.
setUseIc(true)
wantMainModule(conf)
setOutFile(conf)
when not defined(nimKochBootstrap):
if conf.icPreparsedConfig.len == 0:
# `--ic:on` came from a `nim.cfg`/`config.nims` rather than the command
# line, so `nim.nim` could not see it before config loading and the
# precompiled config the children replay does not exist yet. Produce it
# now. (The driver then keeps the config IT parsed instead of replaying
# the artifact; both come from the same files.)
ensureIcConfig(conf)
commandIc(conf)
else:
rawMessage(conf, errGenerated, "--ic:on not available in bootstrap build")
return
setOutFile(conf)
case conf.backend
of backendC: commandCompileToC(graph)
@@ -423,7 +446,9 @@ proc mainCommand*(graph: ModuleGraph) =
# per-module compilation model cannot provide (yet); methods dispatch
# through the classic if-chain dispatchers instead
excl conf.features, Feature.vtables
commandCheck(graph)
# `tStage` for a `nim m` process, so `Process - Stage` is its real startup
# (exec, runtime init, config replay) rather than its whole runtime.
timed tStage: commandCheck(graph)
of cmdNifC:
setUseIc(true)
excl conf.features, Feature.vtables

View File

@@ -61,22 +61,12 @@ proc mangleProcNameExt*(graph: ModuleGraph, s: PSym): string =
# starts with an EMPTY per-name disamb table, so its `disamb` restarts at 0
# and collides with same-named sem-time symbols loaded from NIFs (two
# `=destroy` hooks both mangling to `_u2` → "conflicting types for ..." in
# the generated C). Most such symbols never cross a process boundary (nifc
# lifts, emits and compiles them in one run), so the per-module-unique
# item id is a safe and deterministic discriminator; the `_c` marker keeps
# the namespace disjoint from `_u<disamb>`.
# the generated C). The `_c` marker keeps the namespace disjoint from
# `_u<disamb>`; `backendMintedDisamb` (astdef) is the ONE definition of which
# integer identifies such a symbol, shared with `ccgutils.makeUnique` and
# `ast2nif.toNifSymName` so the C name and the NIF name cannot drift apart.
result = "_c"
if (s.disamb and HookDisambBit) != 0'i32:
# EXCEPTION: a backend-minted sym whose `disamb` is content-derived
# (setHookDisamb gave it HookDisambBit) — e.g. the `rttiDestroy` wrapper —
# DOES cross process boundaries: its C name is baked into the type's RTTI
# table, which is emit-everywhere and merge-deduped, so one process's
# `_c<item>` (a per-process backend counter) ends up referenced while the
# wrapper is defined with another's → undefined at link (`rttiDestroy_c23`).
# The content-derived disamb is stable across processes; use it.
result.addInt s.disamb
else:
result.addInt s.itemId.item
result.addInt backendMintedDisamb(s)
else:
result = "_u"
# Use `disamb` rather than `itemId.item`: under incremental compilation a

View File

@@ -17,7 +17,8 @@ import ast, astalgo, options, lineinfos,idents, btrees, ropes, msgs, pathutils,
when not defined(nimKochBootstrap):
import ast2nif
import "../dist/nimony/src/lib" / [nifstreams, bitabs]
import nifstreams
import "../dist/nimony/src/lib" / bitabs
import typekeys
@@ -35,6 +36,10 @@ type
pureEnums*: seq[PSym]
interf: TStrTable
interfHidden: TStrTable
hiddenPending: bool ## `interfHidden` holds only the exported half so far;
## `ensureHiddenIface` materialises the hidden-only
## symbols on first use. See
## `ast2nif.buildHiddenInterface`.
uniqueName*: Rope
Operators* = object
@@ -136,6 +141,10 @@ type
systemModule*: PSym
sysTypes*: array[TTypeKind, PType]
compilerprocs*: TStrTable
missingCompilerProcs*: HashSet[string]
# `nim nifc` only: compilerproc names no
# loaded module defines, so the whole-program
# index scan in `loadCompilerProc` runs once
exposed*: TStrTable
packageTypes*: TStrTable
emptyNode*: PNode
@@ -165,6 +174,11 @@ type
onDefinitionResolveForward*: proc (graph: ModuleGraph; s: PSym; info: TLineInfo) {.nimcall.}
onUsage*: proc (graph: ModuleGraph; s: PSym; info: TLineInfo) {.nimcall.}
globalDestructors*: seq[PNode]
icModuleDtors*: seq[string] # per-module backend: the C names of the
# other modules' global-destructor procs
# (`genIcModuleDestroyGlobals`), already in
# call order; only the main module's `cg`
# fills this, from the `.c.nif` meta heads
strongSemCheck*: proc (graph: ModuleGraph; owner: PSym; body: PNode) {.nimcall.}
compatibleProps*: proc (graph: ModuleGraph; formal, actual: PType): bool {.nimcall.}
idgen*: IdGenerator
@@ -247,6 +261,25 @@ proc toBase64a(s: cstring, len: int): string =
result.add cb64[a shr 2]
result.add cb64[(a and 3) shl 4]
proc ensureHiddenIface(g: ModuleGraph; pos: int) =
## Materialise a loaded module's hidden-only interface the first time anything
## asks for it. Every READ of `interfHidden` goes through `interfSelect`, so
## guarding those sites is complete.
if g.ifaces[pos].hiddenPending:
when not defined(nimKochBootstrap):
# By SUFFIX: `c.mods` and `g.ifaces` use different FileIndexes for the
# same module (see `buildHiddenInterface`). Into a LOCAL table, because
# loading symbols can grow `g.ifaces` and a `var` alias into it would then
# point at the freed buffer. Cleared only on success, so an import whose
# `.s.bif` does not exist yet is retried rather than written off.
var tab = g.ifaces[pos].interfHidden
if buildHiddenInterface(ast.program,
cachedModuleSuffix(g.config, FileIndex pos), tab):
g.ifaces[pos].interfHidden = tab
g.ifaces[pos].hiddenPending = false
else:
g.ifaces[pos].hiddenPending = false
template interfSelect(iface: Iface, importHidden: bool): TStrTable =
var ret = iface.interf.addr # without intermediate ptr, it creates a copy and compiler becomes 15x slower!
if importHidden: ret = iface.interfHidden.addr
@@ -282,6 +315,7 @@ proc initModuleIter*(mi: var ModuleIter; g: ModuleGraph; m: PSym; name: PIdent):
assert m.kind == skModule
mi.modIndex = m.position
mi.importHidden = optImportHidden in m.options
if mi.importHidden: ensureHiddenIface(g, mi.modIndex)
result = initIdentIter(mi.ti, g.ifaces[mi.modIndex].interfSelect(mi.importHidden), name)
proc nextModuleIter*(mi: var ModuleIter; g: ModuleGraph): PSym =
@@ -289,6 +323,7 @@ proc nextModuleIter*(mi: var ModuleIter; g: ModuleGraph): PSym =
iterator allSyms*(g: ModuleGraph; m: PSym): PSym =
let importHidden = optImportHidden in m.options
if importHidden: ensureHiddenIface(g, m.position)
for s in g.ifaces[m.position].interfSelect(importHidden).data:
if s != nil:
yield s
@@ -305,12 +340,31 @@ proc reexportedModuleSyms*(g: ModuleGraph; m: PSym): seq[(string, string)] =
not seen.containsOrIncl(s.position):
result.add (s.name.s, cachedModuleSuffix(g.config, FileIndex s.position))
proc reexportedLocalSyms*(g: ModuleGraph; m: PSym): seq[ItemId] =
## Symbols DEFINED in `m` that reached `m`'s interface through an explicit
## `export s` rather than through a `*` marker on their declaration.
##
## `semExport` re-exports by `reexportSym`, which adds to the interface table
## and does NOT set `sfExported` — so a symbol can be importable while its
## declaration says otherwise. The NIF writer decides importability from
## `sfExported` alone and therefore missed exactly these. `std/random` does it
## (`proc initRand(): Rand` private, then `since (1, 5, 1): export initRand`),
## which is why `--ic:on` could not compile anything that reached
## `std/tempfiles` — `initRand()` was undeclared in the importer.
result = @[]
for s in g.ifaces[m.position].interf.data:
if s != nil and s.kind != skModule and sfExported notin s.flags and
s.itemId.module == m.position:
result.add s.itemId
proc someSym*(g: ModuleGraph; m: PSym; name: PIdent): PSym =
let importHidden = optImportHidden in m.options
if importHidden: ensureHiddenIface(g, m.position)
result = strTableGet(g.ifaces[m.position].interfSelect(importHidden), name)
proc someSymAmb*(g: ModuleGraph; m: PSym; name: PIdent; amb: var bool): PSym =
let importHidden = optImportHidden in m.options
if importHidden: ensureHiddenIface(g, m.position)
var ti: TIdentIter = default(TIdentIter)
result = initIdentIter(ti, g.ifaces[m.position].interfSelect(importHidden), name)
if result != nil and nextIdentIter(ti, g.ifaces[m.position].interfSelect(importHidden)) != nil:
@@ -343,8 +397,8 @@ iterator procInstCacheItems*(g: ModuleGraph; s: PSym): PInstantiation =
proc getAttachedOp*(g: ModuleGraph; t: PType; op: TTypeAttachedOp): PSym =
## returns the requested attached operation for type `t`. Can return nil
## if no such operation exists.
if g.attachedOps[op].contains(t.itemId):
result = g.attachedOps[op][t.itemId]
if g.attachedOps[op].contains(t.bindingId):
result = g.attachedOps[op][t.bindingId]
elif g.config.cmd in {cmdNifC, cmdM}:
# Fall back to key-based lookup for NIF-loaded hooks
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
@@ -373,7 +427,7 @@ proc setAttachedOp*(g: ModuleGraph; module: int; t: PType; op: TTypeAttachedOp;
# references derived env-field syms that no module's NIF defines
if g.loadedOps[op].getOrDefault(key) == nil:
g.loadedOps[op][key] = value
g.attachedOps[op][t.itemId] = value
g.attachedOps[op][t.bindingId] = value
return
let existing = g.loadedOps[op].getOrDefault(key)
if existing == nil:
@@ -411,7 +465,7 @@ proc setAttachedOp*(g: ModuleGraph; module: int; t: PType; op: TTypeAttachedOp;
break
if not updated:
g.opsLog.add LogEntry(kind: HookEntry, op: op, module: module, key: key, sym: value)
g.attachedOps[op][t.itemId] = value
g.attachedOps[op][t.bindingId] = value
proc setAttachedOp*(g: ModuleGraph; module: int; typeId: ItemId; op: TTypeAttachedOp; value: PSym) =
## Overload that takes ItemId directly, useful for registering hooks from NIF index.
@@ -419,7 +473,7 @@ proc setAttachedOp*(g: ModuleGraph; module: int; typeId: ItemId; op: TTypeAttach
proc setAttachedOpPartial*(g: ModuleGraph; module: int; t: PType; op: TTypeAttachedOp; value: PSym) =
## we also need to record this to the packed module.
g.attachedOps[op][t.itemId] = value
g.attachedOps[op][t.bindingId] = value
proc completePartialOp*(g: ModuleGraph; module: int; t: PType; op: TTypeAttachedOp; value: PSym) {.inline.} =
discard
@@ -441,19 +495,19 @@ proc addNifReplayAction*(g: ModuleGraph; module: int32; n: PNode) =
g.nifReplayActions.mgetOrPut(module, @[]).add n
iterator getMethodsPerType*(g: ModuleGraph; t: PType): PSym =
if g.methodsPerType.contains(t.itemId):
for it in mitems g.methodsPerType[t.itemId]:
if g.methodsPerType.contains(t.bindingId):
for it in mitems g.methodsPerType[t.bindingId]:
yield it
proc getToStringProc*(g: ModuleGraph; t: PType): PSym =
result = g.enumToStringProcs.getOrDefault(t.itemId)
result = g.enumToStringProcs.getOrDefault(t.bindingId)
if result == nil and g.config.cmd in {cmdNifC, cmdM}:
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
result = g.loadedEnumToStringProcs.getOrDefault(key)
assert result != nil
proc setToStringProc*(g: ModuleGraph; t: PType; value: PSym) =
g.enumToStringProcs[t.itemId] = value
g.enumToStringProcs[t.bindingId] = value
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
# Stamp with the module that owns the generated proc, not the enum's def
# module: the def module's process may never have generated it (same
@@ -461,12 +515,12 @@ proc setToStringProc*(g: ModuleGraph; t: PType; value: PSym) =
g.opsLog.add LogEntry(kind: EnumToStrEntry, module: value.itemId.module.int, key: key, sym: value)
iterator methodsForGeneric*(g: ModuleGraph; t: PType): (int, PSym) =
if g.methodsPerGenericType.contains(t.itemId):
for it in mitems g.methodsPerGenericType[t.itemId]:
if g.methodsPerGenericType.contains(t.bindingId):
for it in mitems g.methodsPerGenericType[t.bindingId]:
yield (it[0], it[1])
proc addMethodToGeneric*(g: ModuleGraph; module: int; t: PType; col: int; m: PSym) =
g.methodsPerGenericType.mgetOrPut(t.itemId, @[]).add (col, m)
g.methodsPerGenericType.mgetOrPut(t.bindingId, @[]).add (col, m)
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
let ownerModule = if t.sym != nil: t.sym.itemId.module.int else: module
g.opsLog.add LogEntry(kind: MethodEntry, module: ownerModule, key: key, sym: m)
@@ -481,6 +535,49 @@ proc logMethodDef*(g: ModuleGraph; s: PSym) =
g.opsLog.add LogEntry(kind: MethodEntry, module: s.itemId.module.int,
key: "", sym: s)
proc logCppMember*(g: ModuleGraph; s: PSym) =
## Log a C++ `{.member.}`/`{.virtual.}`/`{.constructor.}` registration (and the
## `importcpp` default-initializer flavour) so the NIF backend can rebuild
## `memberProcsPerType`/`initializersPerType`, which live only in the sem
## process. Without them the per-module backend emitted the struct WITHOUT its
## in-class member declarations and the out-of-class definitions did not match
## ("no declaration matches 'void Doo::memberProc()'").
##
## No type key: `replayCppMember` re-derives the type from the routine's
## signature exactly as `semCppMember` does, so nothing has to survive the
## round trip except the routine itself.
if g.config.cmd in {cmdNifC, cmdM}:
g.opsLog.add LogEntry(kind: CppMemberEntry, module: s.itemId.module.int,
key: "", sym: s)
proc replayCppMember*(g: ModuleGraph; s: PSym) =
## Inverse of `logCppMember`, mirroring `semstmts.semCppMember`'s derivation.
if s == nil or s.typ == nil: return
if sfImportc notin s.flags:
var typ = if sfConstructor in s.flags: s.typ.returnType else: s.typ.firstParamType
if typ != nil and typ.kind == tyPtr and sfConstructor notin s.flags:
typ = typ.elementType
if typ != nil and typ.kind == tyObject:
let procs = addr g.memberProcsPerType.mgetOrPut(typ.bindingId, @[])
for prc in procs[]:
if prc == s: return
procs[].add s
else:
let typ = s.typ.returnType
if typ != nil and typ.kind == tyObject and
typ.bindingId notin g.initializersPerType and s.typ.n != nil:
# The default values sem read off the `nkIdentDefs` live on the param syms.
var call = newTree(nkCall, newSymNode(s))
var isInitializer = s.typ.n.len > 1
for i in 1 ..< s.typ.n.len:
let p = s.typ.n[i]
if p.kind != nkSym or p.sym.ast == nil or p.sym.ast.kind == nkEmpty:
isInitializer = false
break
call.add p.sym.ast
if isInitializer:
g.initializersPerType[typ.bindingId] = call
proc registerLoadedMethod*(g: ModuleGraph; m: PSym) =
## Rebuild the dispatch buckets from a serialized method registration.
## Buckets group the methods sharing a dispatcher; the dispatcher's BODY
@@ -522,12 +619,6 @@ proc logGenericInstance*(g: ModuleGraph; inst: PSym) =
let ownerModule = inst.itemId.module.int
g.opsLog.add LogEntry(kind: GenericInstEntry, module: ownerModule, sym: inst)
const
InstanceDisambBit* = 0x4000_0000'i32
## Set in the `disamb` of routine instances whose value is content-derived
## (see `setInstanceDisamb`); keeps them disjoint from the small counter
## range ordinary symbols draw from, so the NIF name `name.disamb.module`
## stays collision-free within a module.
proc setInstanceDisamb*(g: ModuleGraph; inst, generic: PSym;
concreteTypes: openArray[PType]) =
@@ -566,12 +657,6 @@ proc setInstanceDisamb*(g: ModuleGraph; inst, generic: PSym;
break
inst.disamb = h
const
HookDisambBit* = 0x2000_0000'i32
## Set in the `disamb` of synthesized type-bound operators and `$enum`
## procs whose value is content-derived (see `setHookDisamb`); disjoint
## from both the small counter range and the `InstanceDisambBit` range.
proc setHookDisamb*(g: ModuleGraph; hook: PSym; opName: string; typ: PType) =
## Under IC, replace a synthesized hook's counter-based `disamb` with a
## content-derived one: a hash of the operation name plus the `typeKey` of
@@ -638,6 +723,29 @@ proc loadCompilerProc*(g: ModuleGraph; name: string): PSym =
strTableAdd(g.compilerprocs, result)
return result
# `nim nifc`: a module loaded from a NIF is named by its mangled suffix
# (`thrkxstl4`), not by its source name, and its file index resolves to
# that suffix too — so the `"threadpool"` match below can never fire and
# `spawn`, expanded at codegen time, died on `system module needs:
# nimArgsPassingDone`. The backend loads the WHOLE program before
# codegen starts, so just consult every loaded module's index; a miss is
# final for the rest of the process (nothing more gets loaded) and is
# remembered, because `getCompilerProc` is also used as a mere presence
# probe and would otherwise rescan every index on every call.
if g.config.cmd == cmdNifC:
if name in g.missingCompilerProcs: return nil
for moduleIdx in 0..<g.ifaces.len:
let module = g.ifaces[moduleIdx].module
if module == nil or module.position.FileIndex == systemFileIdx: continue
if not fileExists(toNifFilename(g.config, module.position.FileIndex)):
continue
result = tryResolveCompilerProc(ast.program, name, module.position.FileIndex)
if result != nil:
strTableAdd(g.compilerprocs, result)
return result
g.missingCompilerProcs.incl name
return nil
# Try threadpool module (some compilerprocs like FlowVar are there)
# Find threadpool module by searching loaded modules
for moduleIdx in 0..<g.ifaces.len:
@@ -940,6 +1048,8 @@ when not defined(nimKochBootstrap):
g.loadedOps[x.op][x.key] = x.sym
of EnumToStrEntry:
g.loadedEnumToStringProcs[x.key] = x.sym
of CppMemberEntry:
replayCppMember(g, x.sym)
of MethodEntry:
# only `methodDef` registrations (empty key) rebuild dispatch
# buckets; the `addMethodToGeneric` flavor (typeKey key) announces
@@ -970,7 +1080,13 @@ when not defined(nimKochBootstrap):
var isKnownFile = false
let fileIdx = g.config.registerNifSuffix(string suffix, isKnownFile)
if not g.hookClosure.containsOrIncl(fileIdx.int):
let precomp = loadNifModule(ast.program, suffix, interf, interfHidden, {})
# `SkipInterfaceTables`: `interf`/`interfHidden` here are scratch tables
# shared by every iteration and never read — this module is a
# dep-of-a-dep, so none of its symbols are visible to the module being
# semchecked. Building them called `loadSymFromIndexEntry` for every
# index entry of every closure member.
let precomp = loadNifModule(ast.program, suffix, interf, interfHidden,
{SkipInterfaceTables})
registerLoadedHooks(g, precomp.logOps)
# Record this transitively-loaded module so the sem driver applies its
# VM-level load effects (macro-cache replay + `{.compileTime.}` global init)
@@ -1033,6 +1149,7 @@ when not defined(nimKochBootstrap):
strTableAdd(interf, inner)
g.ifaces[fIdx.int].interf = interf
g.ifaces[fIdx.int].interfHidden = interfHidden
g.ifaces[fIdx.int].hiddenPending = true
proc moduleFromNifFile*(g: ModuleGraph; fileIdx: FileIndex;
flags: set[LoadFlag] = {}): PrecompiledModule =
@@ -1065,11 +1182,26 @@ when not defined(nimKochBootstrap):
setOwner(m, getPackage(g.config, g.cache, fileIdx))
# Register module in graph
registerModule(g, m)
# ... and, in the BACKEND, bind its NIF name to THIS symbol before anything
# in the file is decoded, so the loader never mints a second `skModule` for
# it (see `registerModuleSelfSym`). Backend-only: under `nim m` a module is
# loaded for its INTERFACE, and re-pointing the owner slot of every loaded
# symbol at the freshly built module sym changes what sem sees for an
# imported routine — `times.toDateTimeByWeek` then lost its inferred
# `raises` and the importer failed with "can raise an unlisted exception".
if g.config.cmd == cmdNifC:
registerModuleSelfSym(ast.program, cachedModuleSuffix(g.config, fileIdx), m)
result = loadNifModule(ast.program, fileIdx,
g.ifaces[fileIdx.int].interf,
g.ifaces[fileIdx.int].interfHidden, flags)
# The hidden-only half was not built; `ensureHiddenIface` will, if asked.
g.ifaces[fileIdx.int].hiddenPending = true
result.module = m
# Restore the module symbol's persisted flags (see ast2nif `(modflags)`);
# `cgen.genTopLevelStmt` gates the destructor pass on `sfInjectDestructors`.
if (result.moduleFlags and ModFlagInjectDestructors) != 0:
m.incl sfInjectDestructors
for (mname, msuffix) in result.reexportedModules:
let ms = materializeReexportedModule(g, mname, msuffix)
if ms != nil:
@@ -1117,7 +1249,7 @@ when not defined(nimKochBootstrap):
discard "dispatch buckets already rebuilt by registerLoadedHooks"
of GenericInstEntry:
raiseAssert "GenericInstEntry should not be in the NIF index"
of HookEntry, EnumToStrEntry:
of HookEntry, EnumToStrEntry, CppMemberEntry:
discard "already done by registerLoadedHooks"
# Register methods per type from NIF index
discard "todo"

View File

@@ -28,6 +28,7 @@ import ast, options, lineinfos, modulegraphs, cgendata, cgen,
from cgmeth import generateIfMethodDispatchers
from transf import transformBody
from injectdestructors import injectDestructorCalls
import icprof
import ic / replayer
proc systemNifSuffix(conf: ConfigRef): string =
@@ -134,91 +135,6 @@ proc emitMethodDispatchers(g: ModuleGraph) =
if not containsOrIncl(mainMod.declaredThings, disp.id):
genProcLvl3(mainMod, disp)
proc signatureHasMetaType(t: PType; depth: int = 0): bool =
## Whether a routine signature mentions a compile-time/meta element type
## (`typed`/`untyped` — e.g. `echo`'s `varargs[typed]` — typedesc, static,
## generic param). Such routines are expanded at their call sites and never
## emitted standalone, so the per-module owned-routine seeding must skip them
## (`getTypeDescAux(tyTyped)` otherwise). `tfHasMeta` alone misses the varargs
## element case, hence the explicit scan.
result = false
if t == nil or depth > 8: return false
if t.kind == tyGenericBody:
# The uninstantiated template carried as a `tyGenericInst`'s first child
# always mentions its `tyGenericParam` placeholders, but the instance
# itself is fully concrete (e.g. `var CountTable[SigHash]`). Descending
# here would wrongly flag every routine with a generic-instance parameter
# as meta and drop it from the owned-routine seeding -> undefined symbols
# at link (its only definer never emits it).
return false
if t.kind == tyStatic:
# A RESOLVED static value (the `256` in `MDigest[256]`, the `N` in
# `HashList[T, N]`, …) is carried as a `tyStatic` node inside the otherwise
# fully-concrete `tyGenericInst`, but it is NOT meta: the routine is a normal
# runtime routine the owner must emit. Only an UNRESOLVED `static T` parameter
# (no bound value, `t.n == nil`) is meta. Without this, every routine whose
# signature touches a `static`-parameterized generic instance (the bulk of
# the SSZ/`MDigest` API) is dropped from the owned-routine seeding and ends up
# an undefined reference at link (mirrors the tyGenericBody case above).
return t.n == nil
if t.kind in {tyTyped, tyUntyped, tyTypeDesc, tyGenericParam,
tyAnything, tyFromExpr, tyError}:
return true
for k in t.kids:
if signatureHasMetaType(k, depth + 1): return true
proc ownsRuntimeRoutine(s: PSym; modPos: int): bool =
## A concrete, non-generic, runtime routine with a real body, OWNED by the
## module at `modPos`. Shared by the `cg` stage's owned-routine seeding (so a
## routine called only from other modules is still emitted by somebody) and
## the `lower` stage's owned-routine enumeration, so both stages see exactly
## the same set. The exclusions:
## - nested/closure procs (owner is a proc, not a module): emitted via their
## enclosing routine's lambda-lifting, never standalone;
## - generic instances (`sfFromGeneric`): emitted by demand, deduped by merge;
## - `importc`/`compileTime`/`error`/forward sentinels and meta signatures:
## not real codegen targets.
## - method DISPATCHERS (`sfDispatcher`): their bodies are (re)synthesized into
## the main TU by `emitMethodDispatchers`/`generateIfMethodDispatchers`, never
## per module. A dispatcher is a `copySym` clone of the method that shares the
## method's body sub-tree (incl. its closure iterator); transforming it here
## would lambda-lift that SHARED iterator a SECOND time under a different owner
## identity, baking a conflicting `up` field → "up references do not agree"
## (the divergence is impossible in non-IC, where the dispatcher body is empty
## at lift time). So a dispatcher is never an owned runtime routine.
## A `{.closure.}` iterator IS a standalone runtime routine (unlike an inline
## iterator, which is expanded at each call site) and must be emitted by its
## owner — else a cross-module `for` over it links to nothing.
##
## Generic INSTANCES (`sfFromGeneric`) are NEVER an owned runtime routine — not
## in `cg` and not in the `lower` stage. They are demanded by the backend's
## emit-everywhere path and deduped by `merge` (content C name); the frontend
## materialises them through the `(offer)` mechanism. The `lower` stage must
## not transform an instance: a not-fully-concrete instance (a closure factory
## over a `static` param, or a `$`/`=` op instance whose body resolves only at
## its further-specialised use sites) still carries unresolved overload choices
## and crashes `transformBody` (empty-`namePos` lambda, nil-typed const-fold).
s.itemId.module == modPos and
(s.kind in {skProc, skFunc, skConverter, skMethod} or
(s.kind == skIterator and s.typ != nil and s.typ.callConv == ccClosure)) and
s.skipGenericOwner != nil and s.skipGenericOwner.kind == skModule and
s.magic == mNone and
sfFromGeneric notin s.flags and
sfDispatcher notin s.flags and
{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
# 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.
let moduleId = precomp.module.position
@@ -309,24 +225,30 @@ proc loadBackendModules(g: ModuleGraph; mainFileIdx: FileIndex):
discard setupNifBackendModule(g, precompSys.module)
result = (modules, precompSys, nifFiles)
proc loadDepClosure(g: ModuleGraph; targetSuffix: string):
proc loadDepClosure(g: ModuleGraph; targetSuffixes: seq[string]):
tuple[modules: seq[PrecompiledModule], precompSys: PrecompiledModule,
target: PrecompiledModule] =
## Per-module `cg`/`emit` for a NON-main target: load system + the target
## module + the target's transitive import closure ONLY — not the whole
## program. This is the "process the one file it is passed" model (à la
## Nimony's `hexer c file.nif`): the foreign symbols the target's codegen
## demands are loaded lazily by `ast2nif.moduleId`, which opens any referenced
## module's NIF index on first touch, so a body in a not-loaded module still
## resolves. The closure is loaded as full `BModule`s only so that the
## incidental `g.mods[pos]` accesses during codegen resolve; system's own
## internal closure (allocators, locks, …) is included because a target's
## emit-everywhere codegen can demand those without importing them directly.
targets: seq[PrecompiledModule]] =
## Per-module `lower`/`cg`/`emit` for a NON-main batch: load system + every
## module in the batch + their transitive import closure ONLY — not the whole
## program. This is the "process the files it is passed" model (à la Nimony's
## `hexer c file.nif`): the foreign symbols a target's codegen demands are
## loaded lazily by `ast2nif.moduleId`, which opens any referenced module's NIF
## index on first touch, so a body in a not-loaded module still resolves. The
## closure is loaded as full `BModule`s only so that the incidental
## `g.mods[pos]` accesses during codegen resolve; system's own internal closure
## (allocators, locks, …) is included because a target's emit-everywhere
## codegen can demand those without importing them directly.
##
## The whole program is no longer loaded in this process, which is what bounds
## per-process memory under nifmake's parallel fan-out (the main module's `cg`,
## which still loads everything for NimMain's init list and the method
## dispatchers, runs essentially alone since every other `.c.nif` precedes it).
##
## The batch is loaded as ONE closure: `resetForBackend`, the system load and
## the closure walk happen once no matter how many targets share the process,
## and a module in two targets' closures is loaded once. That amortization is
## the reason batches exist — a per-module process spends far more time here
## than it spends generating code.
resetForBackend(g)
var isKnownFile = false
let systemFileIdx = registerNifSuffix(g.config, systemNifSuffix(g.config), isKnownFile)
@@ -338,18 +260,30 @@ proc loadDepClosure(g: ModuleGraph; targetSuffix: string):
var visited = initHashSet[string]()
visited.incl systemNifSuffix(g.config)
# Only the target is codegen'd, so only it needs its full AST; the closure is
# loaded interface-only (demanded bodies come lazily from the kept-open
# streams), which is what keeps a per-module process light under parallel fan-out.
var isKnown = false
let targetIdx = registerNifSuffix(g.config, targetSuffix, isKnown)
let target = moduleFromNifFile(g, targetIdx, {LoadFullAst})
visited.incl targetSuffix
# Only the batch is codegen'd, so only it needs full ASTs; the surrounding
# closure is loaded interface-only (demanded bodies come lazily from the
# kept-open streams), which is what keeps the process light under fan-out.
var targets: seq[PrecompiledModule] = @[]
var stack: seq[ModuleSuffix] = @[]
if target.module != nil:
modules.add target
for dep in target.deps: stack.add dep
# Separate from `visited`, which exists to keep the closure walk off modules
# already loaded. System is in `visited` from the start yet can perfectly well
# BE a batch member — it is a live node with its own `.t.bif` and `.c.nif` —
# and then it needs the full-AST load like any other member, on top of the
# interface-only load above. Reusing `visited` to deduplicate members skipped
# it and produced a batch with nothing in it.
var claimed = initHashSet[string]()
for targetSuffix in targetSuffixes:
if claimed.containsOrIncl(targetSuffix): continue
var isKnown = false
let targetIdx = registerNifSuffix(g.config, targetSuffix, isKnown)
let target = moduleFromNifFile(g, targetIdx, {LoadFullAst})
targets.add target
# A member that is also another member's dependency must keep its full AST,
# so claim it before the closure walk can load it interface-only.
visited.incl targetSuffix
if target.module != nil:
modules.add target
for dep in target.deps: stack.add dep
if precompSys.module != nil:
for dep in precompSys.deps: stack.add dep
while stack.len > 0:
@@ -366,7 +300,7 @@ proc loadDepClosure(g: ModuleGraph; targetSuffix: string):
discard setupNifBackendModule(g, m.module)
if precompSys.module != nil:
discard setupNifBackendModule(g, precompSys.module)
result = (modules, precompSys, target)
result = (modules, precompSys, targets)
proc findTargetModule(g: ModuleGraph; modules: seq[PrecompiledModule];
precompSys: PrecompiledModule; suffix: string): PrecompiledModule =
@@ -380,6 +314,18 @@ proc findTargetModule(g: ModuleGraph; modules: seq[PrecompiledModule];
cachedModuleSuffix(g.config, FileIndex precompSys.module.position) == suffix:
return precompSys
proc backendBatch(conf: ConfigRef; mainSuffix: string):
tuple[members: seq[string], isMain: bool] =
## The module suffixes this invocation processes, and whether it is the
## main-module invocation. Main is never batched with anything else: it loads
## the WHOLE program (NimMain's init list and the method dispatchers are
## whole-program facts), so putting another module in with it would defeat the
## bound on per-process memory that the per-module split exists to provide.
let members = conf.icBackendModules
result = (members: members,
isMain: members.len == 0 or
(members.len == 1 and members[0] == mainSuffix))
proc setNestedClosureBodies(g: ModuleGraph; idgen: IdGenerator; n: PNode;
owner: PSym; seen: var IntSet) =
## A closure routine nested in `owner` (the `:anonymous` proc lambda-lifting
@@ -444,8 +390,12 @@ proc reownFromTwin(n: PNode; twin, s: PSym) =
for i in 0 ..< n.safeLen:
reownFromTwin(n[i], twin, s)
proc lowerOneModule(g: ModuleGraph; target: PrecompiledModule;
seenNested: var IntSet)
proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
## Per-module backend lowering (`--icBackendStage:lower --icBackendModule:<suffix>`):
## Backend lowering for this invocation's batch
## (`--icBackendStage:lower --icBackendModules:<a,b,c>`):
## enumerate the routines this module OWNS and write them to `<module>.t.nif`.
## Eventually this transforms each owned routine once, in the owner's id space,
## so `cg` reads the result instead of re-deriving it (re-derivation per
@@ -458,30 +408,46 @@ proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
## stage does.
nifcBackendActive = true
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
let targetIsMain = g.config.icBackendModule.len == 0 or
g.config.icBackendModule == mainSuffix
let batch = backendBatch(g.config, mainSuffix)
var modules: seq[PrecompiledModule]
var precompSys: PrecompiledModule
var target: PrecompiledModule
if targetIsMain:
var targets: seq[PrecompiledModule]
if batch.isMain:
var nifFiles: seq[string]
(modules, precompSys, nifFiles) = loadBackendModules(g, mainFileIdx)
if modules.len == 0:
rawMessage(g.config, errGenerated,
"Cannot load NIF file for main module: " & toFullPath(g.config, mainFileIdx))
return
target = findTargetModule(g, modules, precompSys, g.config.icBackendModule)
targets = @[findTargetModule(g, modules, precompSys, mainSuffix)]
else:
(modules, precompSys, target) = loadDepClosure(g, g.config.icBackendModule)
(modules, precompSys, targets) = block:
icProfStart(tLoadClosure)
let r = loadDepClosure(g, batch.members)
icProfStop(tLoadClosure)
r
# ONE PSym graph for the whole batch, so the guard against transforming a
# nested routine twice has to span it: two members reaching the same nested
# closure would otherwise inject its destructors twice into the same `PSym`.
# (In the one-module-per-process fan-out the two members are two processes
# with two copies, and each injects once.)
var seenNested = initIntSet()
for target in targets:
lowerOneModule(g, target, seenNested)
proc lowerOneModule(g: ModuleGraph; target: PrecompiledModule;
seenNested: var IntSet) =
## Lower the routines `target` OWNS and write its `.t.bif`. One batch member.
if target.module == nil:
rawMessage(g.config, errGenerated,
"per-module lowering: module not found for suffix: " & g.config.icBackendModule)
"per-module lowering: module not found for suffix")
return
let modPos = target.module.position
let tb = BModuleList(g.backend).mods[modPos]
if tb == nil:
rawMessage(g.config, errGenerated,
"per-module lowering: no backend module for suffix: " & g.config.icBackendModule)
"per-module lowering: no backend module for suffix: " &
cachedModuleSuffix(g.config, FileIndex modPos))
return
# Transform every owned routine ONCE in this single process's id space and
# re-serialize the ENTIRE module as a proper indexed NIF (`writeLoweredModule`)
@@ -497,11 +463,14 @@ proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
# `transformBody`/lambda-lifting LIFTS the closure env's type-bound ops
# (`=destroy` etc.) into `g.opsLog`; snapshot its length so we serialize exactly
# the ops THIS stage created (not those loaded from `.s.nif`).
# Per MEMBER, not per batch: each member's `.t.bif` must carry exactly the ops
# ITS lowering lifted, the way its own process would have written them.
let opsLogStart = g.opsLog.len
# Shared across the owned loop so a nested routine reachable from more than one
# owner is transformed + destructor-injected EXACTLY once (double injection
# would emit two `=destroy`/`=copy` runs).
var seenNested = initIntSet()
# `seenNested` comes from the caller and spans the whole batch — see the
# comment at its declaration. Within one module it already served to transform
# + destructor-inject a nested routine reachable from more than one owner
# EXACTLY once (double injection would emit two `=destroy`/`=copy` runs).
icProfStart(tLowerOwned)
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
if ownsRuntimeRoutine(s, modPos):
# REUSE path (`icReuseSemLowering` ON): a routine already transformed during
@@ -545,6 +514,8 @@ proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
# into the `.t.nif`; `cg` re-attaches them so `injectDestructorCalls` resolves
# the loaded env's `=destroy`. Iterate to a fixpoint: a hook body can lift
# further hooks (a field's `=destroy`).
icProfStop(tLowerOwned)
icProfStart(tLowerHooks)
var hooks: seq[LogEntry] = @[]
var i = opsLogStart
while i < g.opsLog.len:
@@ -564,9 +535,11 @@ proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
# Re-serialize the whole module to its suffix-based `.t.nif` (the path
# `toNifFilename` resolves for the cg/emit stages). `writeLoweredModule` seals
# routines itself.
icProfStop(tLowerHooks)
let suffix = cachedModuleSuffix(g.config, FileIndex modPos)
let wholeArtifact = toGeneratedFile(g.config, AbsoluteFile(suffix), ".t.bif").string
writeLoweredModule(ast.program, g.config, target, hooks, wholeArtifact)
timed tLowerWrite:
writeLoweredModule(ast.program, g.config, target, hooks, wholeArtifact)
if isDefined(g.config, "icDceCheck"):
stderr.writeLine "[icLower] " & extractFilename(wholeArtifact) & " " &
$hooks.len & " hooks"
@@ -587,13 +560,19 @@ proc visitDep(suffix: string;
let bm = bl.mods[pm.module.position]
if bm != nil: ordered.add bm
proc cgGenerateModule(g: ModuleGraph; target: PrecompiledModule)
proc cgFinishModule(g: ModuleGraph; target: PrecompiledModule;
modules: seq[PrecompiledModule];
precompSys: PrecompiledModule)
proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
## Per-module backend codegen (`--icBackendStage:cg --icBackendModule:<suffix>`):
## generate C for the single module named by `icBackendModule` and write only
## its `.c.nif` artifact (no merge, no `.c` render, no cc/link — those are
## separate nifmake rules).
## Backend codegen for this invocation's batch
## (`--icBackendStage:cg --icBackendModules:<a,b,c>`): generate C for each
## member and write its `.c.nif` artifact (no merge, no `.c` render, no
## cc/link — those are separate nifmake rules).
##
## `findPendingModule` routes every demand into the target (emit-everywhere).
## `findPendingModule` routes a demand to its owner when the owner is in the
## batch and into the demanding TU otherwise (emit-everywhere).
##
## A NON-main target loads only its own import closure (`loadDepClosure`); the
## whole program is no longer pulled into every parallel `cg` process. The main
@@ -603,12 +582,11 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
# gate `newSymNode`'s lazy-type marking to this stage only (see astdef)
nifcBackendActive = true
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
let targetIsMain = g.config.icBackendModule.len == 0 or
g.config.icBackendModule == mainSuffix
let batch = backendBatch(g.config, mainSuffix)
var modules: seq[PrecompiledModule]
var precompSys: PrecompiledModule
var target: PrecompiledModule
if targetIsMain:
var targets: seq[PrecompiledModule]
if batch.isMain:
var nifFiles: seq[string]
(modules, precompSys, nifFiles) = loadBackendModules(g, mainFileIdx)
if modules.len == 0:
@@ -619,24 +597,90 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
# MERGE stage recomputes the one program-wide live set across all `.c.nif`s.
# Running a whole-program liveness pass over all ~260 NIFs in the main `cg`
# would cost ~900 MB for a result the merge stage throws away.
target = findTargetModule(g, modules, precompSys, g.config.icBackendModule)
targets = @[findTargetModule(g, modules, precompSys, mainSuffix)]
else:
# No whole-program load, hence no whole-program DCE: the target emits its
# No whole-program load, hence no whole-program DCE: each member emits its
# full demanded closure and the merge stage drops what is globally dead.
(modules, precompSys, target) = loadDepClosure(g, g.config.icBackendModule)
if target.module == nil:
rawMessage(g.config, errGenerated,
"per-module codegen: module not found for suffix: " & g.config.icBackendModule)
return
(modules, precompSys, targets) = block:
icProfStart(tLoadClosure)
let r = loadDepClosure(g, batch.members)
icProfStop(tLoadClosure)
r
for i, target in targets:
if target.module == nil:
rawMessage(g.config, errGenerated,
"per-module codegen: module not found for suffix: " &
(if i < batch.members.len: batch.members[i] else: mainSuffix))
return
let bl = BModuleList(g.backend)
# Declare which modules this process writes a TU for, BEFORE any code is
# generated: `findPendingModule` consults the set on the very first demand, so
# a member added later would have its definitions routed into whichever TU
# asked first — which is precisely what the set exists to prevent.
for target in targets:
bl.icEmitted.incl target.module.position
# Generate EVERY member before finishing ANY of them. `finishModule` closes a
# TU (`finalCodegenActions` puts it in `modulesClosed`), and a later member's
# codegen routes definitions it does not own INTO an earlier member's TU — see
# `findPendingModule`. Finishing as we went closed those TUs first, and the
# definitions that arrived afterwards were silently dropped: 18 undefined
# symbols at link, all of them `_u`-flagged uniques whose owner happened to
# sort earlier in its batch.
timed tCgGen:
for target in targets:
cgGenerateModule(g, target)
timed tCgFinish:
for target in targets:
cgFinishModule(g, target, modules, precompSys)
# Writes each batch member's `.c.nif` (every other loaded module's TU is empty,
# so `cgenWriteModules` emits no artifact for it). cc/link are NOT run here.
timed tCgWrite:
cgenWriteModules(g.backend, g.config)
# Always leave a `.c.nif` for every member, even one whose module has no code
# (a leaf library whose procs all emit into their users): the nifmake graph
# declares a `.c.nif` output per member, so a missing one would re-fire the
# rule forever. An empty artifact renders to an empty `.c`.
for target in targets:
let tb = bl.mods[target.module.position]
if tb != nil:
let artifact = getCFile(tb).string & ".nif"
if not fileExists(artifact):
writeCnifArtifact("", artifact,
semmedNif = toNifFilename(g.config, FileIndex target.module.position),
moduleBase = $getSomeNameForModule(tb))
proc cgGenerateModule(g: ModuleGraph; target: PrecompiledModule) =
## Generate ONE batch member's code. Does NOT finish its TU — see the caller.
# The `lower` stage already wrote each module's transformed bodies + lifted
# hooks into its `.t.nif`, which the loaders above read directly (toNifFilename
# resolves the `.t.nif`); transformed bodies arrive via loadSymFromCursor and
# lifted hooks via moduleFromNifFile's registerLoadedHooks. Nothing to apply.
generateCodeForModule(g, target)
let bl = BModuleList(g.backend)
if sfMainModule notin target.module.flags:
# This module's top-level `var`s with a `=destroy` registered their teardown
# in `graph.globalDestructors` during `genTopLevelStmt` above. Main's `cg` is
# a different process and never sees them, so emit them as this TU's own
# exported proc and announce the name in the meta head. Stays HERE, in the
# generate pass: it consumes the destructors this module just registered.
let tbm = bl.mods[target.module.position]
if tbm != nil:
tbm.icGlobalDtorName = genIcModuleDestroyGlobals(g, tbm)
proc cgFinishModule(g: ModuleGraph; target: PrecompiledModule;
modules: seq[PrecompiledModule];
precompSys: PrecompiledModule) =
## Close ONE batch member's translation unit, once every member of the batch
## has generated. The artifact write is not here: `cgenWriteModules` is a
## single whole-list operation the caller runs after the whole batch.
let bl = BModuleList(g.backend)
# The main module also owns the whole-program method dispatchers + NimMain.
if sfMainModule in target.module.flags:
icProfStart(tCgInit)
emitMethodDispatchers(g)
# NimMain (generated when the main module is finished) must call every other
# module's init/datInit. Those translation units are produced by their own
@@ -695,24 +739,22 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
for m in ordered:
let heads = readCnifHeads(getCFile(m).string & ".nif")
registerReusedModuleToMain(bl, m, heads.initRequired, heads.datInitRequired)
if heads.globalDtor.len > 0: g.icModuleDtors.add heads.globalDtor
# `ordered` is dependency (post-order) init order; teardown runs in reverse,
# so an importer's globals are destroyed before the ones it may still point
# at. This mirrors whole-program cgen, which walks its single accumulated
# `globalDestructors` list backwards. Main's own destructors come first and
# are added by `finalCodegenActions` itself.
reverse g.icModuleDtors
icProfStop(tCgInit)
let tb = bl.mods[target.module.position]
if tb != nil:
finishModule(g, tb)
# Writes only the target's `.c.nif` (every other loaded module's TU is empty,
# so `cgenWriteModules` emits no artifact for it). cc/link are NOT run here.
cgenWriteModules(g.backend, g.config)
# Always leave a `.c.nif` for the target, even when the module has no code
# (a leaf library whose procs all emit into their users): the per-module
# nifmake graph declares one `.c.nif` output per `cg` rule, so a missing one
# would re-fire the rule forever. An empty artifact renders to an empty `.c`.
if tb != nil:
let artifact = getCFile(tb).string & ".nif"
if not fileExists(artifact):
writeCnifArtifact("", artifact,
semmedNif = toNifFilename(g.config, FileIndex target.module.position),
moduleBase = $getSomeNameForModule(tb))
# Record this module's C compile/link directives next to its `.c` so the
# `link` stage can recover them without loading the module graph. See
# `replayer.writeBackendActions`.
writeBackendActions(g, target.module, target.topLevel,
getCFile(tb).string & BackendActionsExt)
proc generateMergeStage(g: ModuleGraph) =
## Per-module backend merge (`--icBackendStage:merge`): a pure artifact
@@ -724,8 +766,19 @@ proc generateMergeStage(g: ModuleGraph) =
## in-process first-claimant/DCE coordination.
let nimcache = getNimcacheDir(g.config).string
var files: seq[string] = @[]
for artifact in walkFiles(nimcache / "*.c.nif"):
files.add artifact
# The driver lists the live modules' artifacts explicitly (deps.nim's
# `writeLiveModules`); only fall back to globbing when that manifest is
# absent (a cache written by an older compiler). Globbing merges whatever
# `.c.nif` happens to sit in the directory, which is wrong the moment the
# cache is shared with another program — see `LiveModulesFile`.
let manifest = nimcache / LiveModulesFile
if fileExists(manifest):
for line in lines(manifest):
let p = line.strip()
if p.len > 0: files.add p
else:
for artifact in walkFiles(nimcache / ("*" & icCFileExt(g.config) & ".nif")):
files.add artifact
sort files
let decision = computeMergeDecision(files)
if decision.broken:
@@ -738,16 +791,19 @@ proc generateMergeStage(g: ModuleGraph) =
" live: " & $decision.live.len & " defs: " & $decision.defs &
" liveDefs: " & $decision.liveDefs & " owned: " & $decision.owners.len
proc emitOneModule(g: ModuleGraph; mainFileIdx: FileIndex; member: string;
isMain: bool; decision: MergeDecision)
proc generateEmitStage(g: ModuleGraph; mainFileIdx: FileIndex) =
## Per-module backend emit (`--icBackendStage:emit --icBackendModule:<suffix>`):
## Backend emit for this invocation's batch
## (`--icBackendStage:emit --icBackendModules:<a,b,c>`):
## render the target module's final `.c` from its `.c.nif` and the merge
## decision. Loads the target the same way `cg` does so `getCFile` returns the
## identical path `cg` wrote to (the main module's source-vs-suffix aliasing in
## particular); no codegen runs. A non-main target loads only its own closure
## (`loadDepClosure`) so emit, like `cg`, stays bounded under parallel fan-out.
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
let targetIsMain = g.config.icBackendModule.len == 0 or
g.config.icBackendModule == mainSuffix
let batch = backendBatch(g.config, mainSuffix)
# emit renders a module's final `.c` PURELY from its own `.c.nif` and the merge
# decision (see `renderCFromArtifact` — text filtering, no AST is touched). It
# used to load the target's whole transitive import closure as BModules solely
@@ -760,21 +816,38 @@ proc generateEmitStage(g: ModuleGraph; mainFileIdx: FileIndex) =
# path directly instead — the SAME pure computation `deps.nim.backendCFile`
# uses to DECLARE this stage's output (`getCFile` == that formula) — so an emit
# process loads nothing and the fire-all costs process-startup, not a graph load.
let cfilename =
if targetIsMain: AbsoluteFile toFullPath(g.config, mainFileIdx)
else: AbsoluteFile g.config.icBackendModule
let cfile = changeFileExt(completeCfilePath(g.config,
mangleModuleName(g.config, cfilename).AbsoluteFile), ".nim.c").string
let artifact = cfile & ".nif"
if not fileExists(artifact):
rawMessage(g.config, errGenerated,
"per-module emit: missing .c.nif artifact for suffix: " & g.config.icBackendModule)
return
# The decision is read ONCE for the batch: it is a whole-program artifact, and
# re-reading it per member was a per-process cost the batch exists to remove.
let decision = readMergeDecision(getNimcacheDir(g.config).string / MergeDecisionFile)
if decision.broken:
rawMessage(g.config, errGenerated,
"per-module emit: missing or unparsable merge decision " & MergeDecisionFile)
return
let members = if batch.members.len == 0: @[mainSuffix] else: batch.members
for member in members:
# Per MEMBER, not per batch. `backendBatch.isMain` answers "is this
# invocation the main-module invocation", which is the right question for
# `lower`/`cg` (main loads the whole program, so it is never batched with
# anything). emit has no such constraint and batches freely, so main can sit
# in a batch with others — and then the batch-wide flag sent main's `.c` to
# the path derived from its SUFFIX rather than from its source file, and its
# `.c` was never written.
emitOneModule(g, mainFileIdx, member, member == mainSuffix, decision)
proc emitOneModule(g: ModuleGraph; mainFileIdx: FileIndex; member: string;
isMain: bool; decision: MergeDecision) =
## Render ONE batch member's final `.c` from its `.c.nif` and the batch's
## merge decision.
let cfilename =
if isMain: AbsoluteFile toFullPath(g.config, mainFileIdx)
else: AbsoluteFile member
let cfile = changeFileExt(completeCfilePath(g.config,
mangleModuleName(g.config, cfilename).AbsoluteFile), icCFileExt(g.config)).string
let artifact = cfile & ".nif"
if not fileExists(artifact):
rawMessage(g.config, errGenerated,
"per-module emit: missing .c.nif artifact for suffix: " & member)
return
var dropped = 0
let code = renderCFromArtifact(artifact, decision, extractFilename(artifact), dropped)
# Write the `.c` content-stably. `merge` re-runs on any edit and bumps the
@@ -788,6 +861,15 @@ proc generateEmitStage(g: ModuleGraph; mainFileIdx: FileIndex) =
# up-to-date check, not a shared prerequisite in nifmake's mtime ordering.
if not fileExists(cfile) or readFile(cfile) != code:
writeFile(cfile, code)
# ... but nifmake needs SOME output whose mtime proves "this rule ran since its
# inputs last moved". With the `.c` as the only output, the content-stable write
# above is indistinguishable from not having run: `merge` rewrites the decision
# file unconditionally, so every `emit` whose `.c` came out byte-identical stays
# older than a declared input and re-fires on every warm build from then on
# (measured: all 218 emit rules of a 219-module program, on a NO-OP build).
# The stamp is written unconditionally and is the rule's freshness proof; the
# `.c` keeps its content-stable mtime so `callCCompiler` still reuses the `.o`.
writeFile(cfile & ".stamp", $code.len & " " & $dropped & "\n")
if isDefined(g.config, "icDceCheck"):
stderr.writeLine "[icEmit] " & extractFilename(cfile) & " dropped " &
$dropped & " bodies (" & $code.len & " bytes)"
@@ -796,64 +878,74 @@ proc generateLinkStage(g: ModuleGraph; mainFileIdx: FileIndex) =
## Per-module backend link (`--icBackendStage:link`): the `emit` stages have
## written every module's `.c`; register them and run the C compiler + linker
## once via `extccomp.callCCompiler` (which parallelizes the per-file cc and
## skips up-to-date objects itself). No codegen runs — the graph is loaded only
## so `getCFile` yields each module's emitted `.c` path.
let (modules, precompSys, _) = loadBackendModules(g, mainFileIdx)
if modules.len == 0:
rawMessage(g.config, errGenerated,
"Cannot load NIF file for main module: " & toFullPath(g.config, mainFileIdx))
return
# The per-module `cg` processes each collect their module's C compile/link
# directives (`{.passL: "-lm".}` etc.) via `replayBackendActions`, but those
# live in the cg process and never reach this separate link process. Re-collect
# every loaded module's directives here so the final `callCCompiler` sees them
# (without this, math's `-lm` is lost → undefined `floor`/`pow`/… at link).
for m in modules:
replayBackendActions(g, m.module, m.topLevel)
if precompSys.module != nil:
replayBackendActions(g, precompSys.module, precompSys.topLevel)
let bl = BModuleList(g.backend)
## skips up-to-date objects itself). No codegen runs and NO MODULE GRAPH IS
## LOADED.
##
## It used to load the whole import closure (`loadBackendModules`) for two
## things only: each module's `.c` path via `getCFile`, and its recorded C
## directives via `replayBackendActions`. That was 3.7s of the ~11s serial
## backend critical path on a 219-module program — a whole-program
## deserialization to recover a list of paths and a handful of strings. Both
## are now read from artifacts the earlier stages already produce:
## * the driver's `LiveModulesFile` manifest lists every live module's
## `.c.nif`, and the `.c` sits beside it (`emit`'s output);
## * each module's `cg` wrote its directives to a `.cflags` sidecar.
let nimcache = getNimcacheDir(g.config).string
var cfiles: seq[string] = @[]
let manifest = nimcache / LiveModulesFile
if fileExists(manifest):
for line in lines(manifest):
let p = line.strip()
if p.len > 0 and p.endsWith(".nif"): cfiles.add p[0 ..< p.len - ".nif".len]
else:
# A cache written by an older compiler has no manifest; fall back to the
# `.c` files sitting next to the artifacts.
for artifact in walkFiles(nimcache / ("*" & icCFileExt(g.config) & ".nif")):
cfiles.add artifact[0 ..< artifact.len - ".nif".len]
sort cfiles
var addedCFiles = initHashSet[string]()
for m in bl.mods:
if m != nil:
let cfile = getCFile(m)
# Only modules that are their own cg/emit target produced a `.c`; the rest
# (extra members of system's closure that no build rule targets) had their
# code emit-everywhere'd into the targets, so they have no file to compile.
if not fileExists(cfile.string): continue
addedCFiles.incl extractFilename(cfile.string)
var cf = Cfile(nimname: m.module.name.s, cname: cfile,
obj: completeCfilePath(g.config, toObjFile(g.config, cfile)),
flags: {})
# `addExternalFileToCompile` (not `addFileToCompile`) gates each `.c` on its
# SHA1 footprint: an unchanged `.c` keeps its `.o` and is flagged Cached, so
# `callCCompiler` skips its compile but still links the existing object. This
# is what makes a localized edit recompile only the handful of `.c`s the
# `emit` stage actually rewrote, instead of every object every time — the
# final piece of per-module backend incrementality after the merge barrier.
addExternalFileToCompile(g.config, cf)
for cpath in cfiles:
# Only modules that are their own cg/emit target produced a `.c`; the rest
# had their code emit-everywhere'd into the targets, so there is nothing to
# compile for them.
if not fileExists(cpath): continue
addedCFiles.incl extractFilename(cpath)
# The directives this module recorded (`{.passL: "-lm".}` etc.); without
# them math's `-lm` is lost -> undefined `floor`/`pow`/… at link.
applyBackendActions(g, cpath & BackendActionsExt)
let cfile = AbsoluteFile cpath
var cf = Cfile(nimname: splitFile(cfile).name, cname: cfile,
obj: completeCfilePath(g.config, toObjFile(g.config, cfile)),
flags: {})
# `addExternalFileToCompile` (not `addFileToCompile`) gates each `.c` on its
# SHA1 footprint: an unchanged `.c` keeps its `.o` and is flagged Cached, so
# `callCCompiler` skips its compile but still links the existing object. This
# is what makes a localized edit recompile only the handful of `.c`s the
# `emit` stage actually rewrote, instead of every object every time.
addExternalFileToCompile(g.config, cf)
# deps.nim's static scanner can keep a CONDITIONALLY-imported module as a build
# node (e.g. `net`'s `when defineSsl: import openssl`, or a `when defined(os)`
# import) that the NIF-`deps` walk above never reaches because the condition is
# off. Such a node still emitted a `.c`, and it can OWN a live generic instance
# that a REACHABLE module reuses (openssl owns `toHex[uint8]`, reused by
# `strutils.escape`) — so its body must be at link or that reference is
# node (e.g. `net`'s `when defineSsl: import openssl`) that the manifest above
# may not cover. Such a node still emitted a `.c`, and it can OWN a live generic
# instance that a REACHABLE module reuses (openssl owns `toHex[uint8]`, reused
# by `strutils.escape`) — so its body must be at link or that reference is
# undefined. Link every emitted `.c` the merge decision says OWNS a LIVE symbol;
# a node that owns nothing live (a Windows-only winsock node on Linux) is
# correctly skipped.
block:
let nimcache = getNimcacheDir(g.config).string
let decision = readMergeDecision(nimcache / MergeDecisionFile)
if not decision.broken:
var liveOwners = initHashSet[string]()
for cname, owner in decision.owners:
if owner.endsWith(".c.nif") and cname in decision.live:
if owner.endsWith(icCFileExt(g.config) & ".nif") and cname in decision.live:
liveOwners.incl owner
for owner in liveOwners:
let cbase = owner[0 ..< owner.len - ".nif".len] # "@m….nim.c.nif" -> ".c"
if addedCFiles.containsOrIncl(cbase): continue
let cfile = AbsoluteFile(nimcache / cbase)
if not fileExists(cfile.string): continue
applyBackendActions(g, cfile.string & BackendActionsExt)
var cf = Cfile(nimname: cbase, cname: cfile,
obj: completeCfilePath(g.config, toObjFile(g.config, cfile)),
flags: {})
@@ -864,20 +956,27 @@ proc generateLinkStage(g: ModuleGraph; mainFileIdx: FileIndex) =
proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) =
## Main entry point for NIF-based C code generation.
## Traverses the module dependency graph and generates C code.
when defined(icBNodeProf): profStageName = g.config.icBackendStage
if g.config.icBackendStage == "lower":
generateLowerStage(g, mainFileIdx)
timed tStage: generateLowerStage(g, mainFileIdx)
return
elif g.config.icBackendStage == "cg":
generateCgStage(g, mainFileIdx)
timed tStage: generateCgStage(g, mainFileIdx)
return
elif g.config.icBackendStage == "merge":
generateMergeStage(g)
timed tStage:
timed tMergeStage:
generateMergeStage(g)
return
elif g.config.icBackendStage == "emit":
generateEmitStage(g, mainFileIdx)
timed tStage:
timed tEmitRender:
generateEmitStage(g, mainFileIdx)
return
elif g.config.icBackendStage == "link":
generateLinkStage(g, mainFileIdx)
timed tStage:
timed tLinkStage:
generateLinkStage(g, mainFileIdx)
return
else:
rawMessage(g.config, errGenerated,

247
compiler/nifstreams.nim Normal file
View File

@@ -0,0 +1,247 @@
## nifstreams — the classic NIF streaming surface, used ONLY by this compiler's
## IC modules: ast2nif, deps, modulegraphs and pipelines import it and must keep
## compiling unchanged across nimony's own refactorings.
##
## It used to live in `dist/nimony/src/lib`, which is where the rest of the NIF
## stack still is. It does not belong there: nimony's own code imports nifpools
## (via nifprelude) and is under standing orders never to import this file, so
## nothing over there ever exercised it — which is exactly how it came to hand
## out `TagLit` where every caller here tests for `ParLe` (see `next`), silently
## emptying the IC build graph. A compatibility shim with exactly one consumer
## belongs in the consumer's repo, where its tests run and its contract is
## somebody's problem.
##
## Everything it adapts (`nifpools`, `nifreader`, `lineinfos`) still comes from
## `dist/nimony`; only the adapter moved.
##
## Everything here is an honest adapter, not a fake:
## * Floats get a REAL interning pool: `pool.floats.getOrIncl` returns a
## `FloatId` index, `floatToken` packs it into a genuine `FloatLit` NifToken
## (transit-only: it must never enter a TokenBuf, whose float encoding is
## inline multi-token), and `pool.floats[t.floatId]` decodes it — lossless.
## * `Stream`/`next` wrap the textual nifreader; the unified NifKind has real
## `ParLe`/`ParRi`/`EofToken` members, so structural scanners (deps.nim)
## see the exact classic kinds. Ident/StringLit/Symbol payloads are interned
## into the global `pool`, so `pool.strings[t.litId]` works as before.
## Number tokens keep their KIND only (a 4-byte token cannot always carry
## the value); classic scanners never read those payloads.
import std / tables
import "../dist/nimony/src/lib" / nifpools
# `except`: the frontend went all-NifLineInfo; the classic side keeps speaking
# PackedLineInfo, so nifpools' same-name/same-params variants must not leak
# through (`info(n: NifToken)` differs only in return type, `NoLineInfo` is a
# same-name const of a different type — either would be ambiguous or wrong for
# ast2nif). The classic replacements are defined below / come from lineinfos.
# `tagId` is excluded for a different reason: nifpools decodes the 9-bit field
# of a real `TagLit`, but this surface hands out `ParLe` tokens whose tag id
# fills the whole 28-bit payload (see `next`), so the decode below is the only
# correct one here.
export nifpools except info, NoLineInfo, tagId
import "../dist/nimony/src/lib" / lineinfos
export lineinfos
from "../dist/nimony/src/lib" / nifreader import Reader, ExpandedToken, decodeStr
# ── Classic names the Nim compiler side still uses ───────────────────────
type
PackedToken* = NifToken ## ast2nif still says PackedToken
# Raw payload decodes, sound ONLY on this surface. Every token here comes from
# `next` or the classic `symToken`/`strToken`/`identToken` constructors, which
# intern EVERY literal — including names of at most `StrInlineMaxLen` bytes,
# which the nifcore builders would instead store inside the token. On such an
# inline token the payload is packed bytes, not an id, so nifpools (nimony's own
# surface, where buffers come from the builders) deliberately has no equivalent:
# there it must go through a `Cursor`, which handles both encodings.
proc tagId*(n: NifToken): TagId {.inline.} = TagId(uoperand(n))
## Classic `ParLe` tokens (see `next`) keep the tag id in the full 28-bit
## payload rather than in `TagLit`'s 9-bit field: `globalTags` already holds
## 355 tags before the Nim compiler registers its own dialect, so a 512-tag
## ceiling is not a ceiling this surface can live under.
proc litId*(n: NifToken): StrId {.inline.} = StrId(uoperand(n) shr 1)
proc symId*(n: NifToken): SymId {.inline.} = SymId(uoperand(n) shr 1)
proc litId*(c: Cursor): StrId {.inline.} = strId(c)
proc firstSon*(n: Cursor): Cursor {.inline.} = childCursor(n)
var lineMan*: LineInfoManager
## The classic packed line-info side channel (`pool.man`). Frontend code no
## longer uses it — it lives here purely for ast2nif's writer, which packs
## `TLineInfo` into `PackedLineInfo` and unpacks on emit.
template files*(p: Pool): untyped = p.filenames
template tags*(p: Pool): untyped = globalTags.tags
template man*(p: Pool): untyped = lineMan
proc info*(n: NifToken): PackedLineInfo {.inline.} = lineinfos.NoLineInfo
## Classic tokens carried their line info inline; a bare 4-byte nifcore
## token cannot, so reading it back yields `NoLineInfo` (ast2nif's
## `emitInfo(t.info)` then emits nothing — matching the writer, which
## attaches real positions at the builder level instead).
proc info*(c: Cursor): PackedLineInfo {.inline.} =
## Classic packed view of a cursor's line info (ast2nif shadows this with
## its own NifLineInfo template; kept for any other classic reader).
let li = rawLineInfo(c)
if li.file.isValid: pack(lineMan, li.file, li.line, li.col)
else: lineinfos.NoLineInfo
type
IntId* = distinct int64 ## value carriers (nifcore stores inline)
UIntId* = distinct uint64
## Identity proxies: the id already carries the value, `[]` returns it.
IntegersProxy* = object
UIntegersProxy* = object
func `==`*(a, b: IntId): bool {.borrow.}
func `==`*(a, b: UIntId): bool {.borrow.}
template integers*(p: Pool): IntegersProxy = IntegersProxy()
template uintegers*(p: Pool): UIntegersProxy = UIntegersProxy()
template `[]`*(x: IntegersProxy; id: IntId): int64 = int64(id)
template `[]`*(x: UIntegersProxy; id: UIntId): uint64 = uint64(id)
# nifcore stores integers inline: the "id" is the value itself.
template getOrIncl*(x: IntegersProxy; v: int64): IntId = IntId(v)
template getOrIncl*(x: UIntegersProxy; v: uint64): UIntId = UIntId(v)
proc intId*(n: NifToken): IntId {.inline.} = IntId(n.soperand)
proc uintId*(n: NifToken): UIntId {.inline.} = UIntId(uoperand(n))
proc intId*(c: Cursor): IntId {.inline.} = IntId(intVal(c))
proc uintId*(c: Cursor): UIntId {.inline.} = UIntId(uintVal(c))
proc addIntLit*(dest: var TokenBuf; id: IntId; info: PackedLineInfo) =
addIntLit(dest, int64(id))
if info.isValid:
let u = unpack(lineMan, info)
appendLineInfo(dest, u.file, u.line, u.col)
# Classic single-token constructors with a (dropped) line-info argument.
proc strToken*(s: StrId; info: PackedLineInfo): NifToken {.inline.} = strLitToken(s)
proc symToken*(id: SymId; info: PackedLineInfo): NifToken {.inline.} = symToken(id)
proc identToken*(id: StrId; info: PackedLineInfo): NifToken {.inline.} = identToken(id)
proc dotToken*(info: PackedLineInfo): NifToken {.inline.} = dotToken()
proc charToken*(ch: char; info: PackedLineInfo): NifToken {.inline.} = charToken(ch)
# ── Classic interned float literals (ast2nif) ────────────────────────────
type
FloatId* = distinct uint32 ## 1-based index into the global float pool
FloatPool* = object
values: seq[float64]
lookup: Table[uint64, uint32] # bit pattern -> 1-based id
func `==`*(a, b: FloatId): bool {.borrow.}
var globalFloats*: FloatPool
template floats*(p: Pool): var FloatPool = globalFloats
proc getOrIncl*(fp: var FloatPool; v: float64): FloatId =
let bits = cast[uint64](v)
let existing = fp.lookup.getOrDefault(bits, 0'u32)
if existing != 0'u32:
result = FloatId(existing)
else:
fp.values.add v
let id = uint32(fp.values.len)
fp.lookup[bits] = id
result = FloatId(id)
proc `[]`*(fp: FloatPool; id: FloatId): float64 {.inline.} =
fp.values[int(uint32(id)) - 1]
proc floatToken*(id: FloatId; info: PackedLineInfo): NifToken {.inline.} =
## Transit-only token: carries the pool index so the receiver can decode it
## via `pool.floats[t.floatId]`. It must never be appended to a TokenBuf
## (nifcore stores floats inline as a multi-token encoding); the line info
## is dropped like in the other classic token constructors.
NifToken((uint32(id) shl KindBits) or uint32(FloatLit))
proc floatId*(n: NifToken): FloatId {.inline.} = FloatId(uoperand(n))
# ── Classic streaming text reader (deps.nim) ─────────────────────────────
type
Stream* = object
r*: Reader
proc parLeToken*(t: TagId): NifToken {.inline.} =
## The classic surface's opening-tag token: kind `ParLe`, tag id in the
## payload. Transit-only, like `floatToken` — a `ParLe` never appears in a
## binary token stream, so this must not be appended to a TokenBuf.
NifToken((uint32(t) shl KindBits) or uint32(ParLe))
proc open*(filename: string): Stream =
Stream(r: nifreader.open(filename))
proc close*(s: var Stream) =
nifreader.close(s.r)
proc next*(s: var Stream): NifToken =
## One classic packed token per call. Pool-referencing kinds are interned
## into the global `pool`/`globalTags`, so `.litId`/`.tagId` accessors and
## `pool.strings[...]`/`pool.tags[...]` lookups behave exactly as classic
## nifstreams did. Kinds without a pool payload come back kind-only.
var t = default(ExpandedToken)
nifreader.next(s.r, t)
case t.tk
of ParLe:
# NOT `tagLitToken`: that would set the kind to `TagLit`, and every classic
# structural scanner tests for `ParLe` (deps.nim walks the import graph that
# way). Emitting `TagLit` here made every one of those tests silently fail —
# the scanner saw an unknown token, skipped the subtree, and the Nim
# compiler's IC build graph came out missing most of its edges.
result = parLeToken(registerTag(globalTags, decodeStr(s.r, t)))
of Ident:
result = identToken(pool.strings.getOrIncl(decodeStr(s.r, t)))
of StrLit:
result = strLitToken(pool.strings.getOrIncl(decodeStr(s.r, t)))
of Symbol:
result = symToken(pool.syms.getOrIncl(decodeStr(s.r, t)))
of SymbolDef:
result = symdefToken(pool.syms.getOrIncl(decodeStr(s.r, t)))
else:
# ParRi/EofToken/DotToken/CharLit/numbers: correct kind, no payload.
result = NifToken(uint32(t.tk))
when isMainModule:
# `nim c -r compiler/nifstreams.nim`.
#
# The promise this checks: structural scanners see the CLASSIC kinds. Nim's deps.nim walks
# the import graph by testing `t.kind == ParLe` and then reading
# `pool.tags[t.tagId]`. Hand out nifcore's own `TagLit` instead and every one
# of those tests falls through silently — the scanner treats the opener as an
# unknown token, skips the subtree, and Nim's IC build graph comes out missing
# most of its edges while each individual file still "parses" fine.
import std / [os, syncio]
from "../dist/nimony/src/lib" / nifreader import processDirectives
from std / assertions import assert
let f = getTempDir() / "nifstreams_selftest.nif"
syncio.writeFile f, "(.nif27)\n(stmts (import (infix / std (bracket os osproc))) (x \"s\" y))\n"
var kinds: seq[NifKind] = @[]
var tagNames: seq[string] = @[]
var lits: seq[string] = @[]
var s = nifstreams.open(f)
discard processDirectives(s.r)
while true:
let t = next(s)
if t.kind == EofToken: break
kinds.add t.kind
case t.kind
of ParLe: tagNames.add pool.tags[t.tagId]
of Ident, StrLit: lits.add pool.strings[t.litId]
else: discard
nifstreams.close(s)
removeFile f
assert tagNames == @["stmts", "import", "infix", "bracket", "x"], $tagNames
assert lits == @["/", "std", "os", "osproc", "s", "y"], $lits
assert ParRi in kinds, "closers must stay classic too"
assert TagLit notin kinds, "an opener must arrive as ParLe, not TagLit"
echo "success"

View File

@@ -120,7 +120,7 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
# so `loadConfigs` replays it instead of re-parsing the `nim.cfg` chain — the
# driver runs on the exact same config its children will. See icconfig.nim.
when not defined(nimKochBootstrap):
if conf.cmd in {cmdIc, cmdTrack}:
if conf.cmd in {cmdIc, cmdTrack} or isIcDriver(conf):
ensureIcConfig(conf)
var graph = newModuleGraph(cache, conf)

340
compiler/nodebridge.nim Normal file
View File

@@ -0,0 +1,340 @@
#
#
# The Nim Compiler
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## `PNode` <-> `TokenBuf`, in one process.
##
## WHY THIS EXISTS. The backend splits in two along a line that is not the one
## the migration to `BNode` was drawn along. Passes that REWRITE — transf,
## destructor injection, closure lifting, the tree the code generator builds as
## it goes — construct new nodes, and a `Cursor` is a read cursor into a shared
## token buffer, so they cannot be expressed against it and there is no reason
## to try. Passes that READ want the cursor. The bridge is the seam between
## them: a rewriting pass keeps producing a `PNode`, and anything that only
## reads gets a `TokenBuf`, from which a `Cursor` — and so a `BNode` — is a
## pointer.
##
## HOW IT DIFFERS FROM THE `.bif` FORMAT, and why that is the point. A `.bif`
## is read by a DIFFERENT PROCESS, so every symbol and type has to be written as
## a NAME the reader can look up again. A bridged buffer is read by the process
## that built it, so it does not: a symbol reference is `(bsym <idx>)`, an index
## into a side table holding the very `PSym` the encoder was handed, and the
## type slot is `(btyp <idx>)` the same way.
##
## Three consequences, and the middle one is the reason to prefer this over
## routing rewrites back through the file format:
##
## * It is LOSSLESS. No name mangling, no module index, no stubs, so nothing can
## be lost or renamed on the way through. `toPNode(toTokenBuf(n))` is `n`
## again, and `cgen`'s grinder checks the stronger property — that the cursor
## answers identically to the ORIGINAL `PNode` at every node, with no
## tolerated differences at all, unlike the file path which needs two.
## * `sym` IS IDEMPOTENT HERE, FIELDS INCLUDED. On the file path it is not, and
## cannot be: a cross-context field reference has no index entry, so
## `loadFieldStub` mints a fresh `skField` stub per use because two distinct
## fields can share a name and a position across types. That is what blocks
## `aliases.isPartOf` from moving to the seam (see `bnode.sym`). A bridged
## buffer hands back the same object every time, so code that compares field
## identity is correct on it.
## * The ENCODER is cheap, and that part is measured: no string formatting, no
## pool lookups for names, no index seeks, just a tree walk and two `seq.add`s.
## Building a buffer for every routine and NOT reading it costs 6.79s against a
## 6.75s baseline on a 50-module target — inside the noise.
##
## READING is not free, and that is where the cost of the whole seam sits.
## Driving the generator off cursors takes the same target from 6.75s to 8.85s,
## **+31%**, stable across interleaved runs. Since a compile is mostly frontend,
## codegen itself is slowed by considerably more than 31%. The suspects are the
## per-access costs a `PNode` does not have: `son(n, i)` is O(i) because it skips
## from the first child, `kind` checks the tag pool and indexes a memo on every
## call, `sym`/`typ` go through the nav, and `origin` is a hash lookup on every
## location built. None of that is inherent — `son` could cache, `origin` could
## key on something cheaper — but none of it has been optimised, and the number
## is here so nobody has to rediscover it before deciding whether to.
##
## WHAT IT IS NOT. The buffer is transient and process-local: `(bsym …)` means
## nothing without the tables beside it, so a bridged buffer must never be
## written to a file. The `.bif` writer in `ast2nif` is still the only thing
## that serializes, and it is a different job — it has to name things precisely
## because the reader cannot see this process's heap.
##
## USE:
##
## var b = toTokenBuf(n, conf)
## withBridge(b.tables):
## let root = BNode(b.rootCursor) # read it like any other `BNode`
## ...
## let back = toPNode(b) # a fresh `PNode` tree, if a rewrite needs one
##
## `withBridge` and `BNode` live in `bnode.nim` and exist only under
## `-d:newIcBackend`; this module is below that seam and does not depend on it,
## so the encoder and the round trip are usable either way.
import std / tables
import ast, astdef, idents, options, msgs, lineinfos
import icnifcore, ast2nif
import ic / enum2nif
import "../dist/nimony/src/lib/nifcore" except pool
import bodynav
when defined(nimPreviewSlimSystem):
import std / assertions
type
BridgeBuf* = object
## An encoded tree plus everything needed to read it back. Not copyable —
## it owns a `TokenBuf`.
bld*: IcBuilder
tables*: BridgeTables
conf: ConfigRef
symIdx: Table[int, int] ## PSym identity -> index into `tables.syms`
typeIdx: Table[int, int] ## PType identity -> index into `tables.types`
proc initBridgeBuf*(conf: ConfigRef; cap = 64): BridgeBuf =
BridgeBuf(bld: newIcBuilder(cap), tables: BridgeTables(), conf: conf,
symIdx: initTable[int, int](), typeIdx: initTable[int, int]())
# ---------------------------------------------------------------------------
# Encode
#
# The shape mirrors the `.bif` node encoding exactly — `(<kind> <flags> <type>
# <child|payload>…)` — so `bnode` reads a bridged buffer with the accessors it
# already has. Only the two leaves that would have been NAMES differ.
proc symIndex(b: var BridgeBuf; s: PSym): int =
## Symbols are deduplicated by identity, so the same `PSym` referenced twenty
## times costs one table slot and twenty equal indices — which is also what
## makes `sym` idempotent on the way back.
let key = cast[int](s)
result = b.symIdx.getOrDefault(key, -1)
if result < 0:
result = b.tables.syms.len
b.tables.syms.add s
b.symIdx[key] = result
proc typeIndex(b: var BridgeBuf; t: PType): int =
let key = cast[int](t)
result = b.typeIdx.getOrDefault(key, -1)
if result < 0:
result = b.tables.types.len
b.tables.types.add t
b.typeIdx[key] = result
proc emitInfo(b: var BridgeBuf; info: TLineInfo) =
## Line info goes through the SAME filename pool the `.bif` writer uses
## (`icPool.filenames`, keyed by full path), so `bnode.info` — which resolves
## through the decoder's `oldLineInfo` — needs no bridge-specific path.
if info == unknownLineInfo: return
b.bld.lineInfo(msgs.toFullPath(b.conf, info.fileIndex),
info.line.int32, info.col.int32)
proc emitFlags(b: var BridgeBuf; flags: TNodeFlags) =
var asIdent = ""
genFlags(flags, asIdent)
if asIdent.len > 0: b.bld.addIdent asIdent
else: b.bld.addDotToken()
proc emitTypeSlot(b: var BridgeBuf; t: PType) =
if t == nil:
b.bld.addDotToken()
else:
b.bld.openTag bridgeTypeTagName
b.bld.addIntLit typeIndex(b, t).int64
b.bld.closeTag()
proc encodeNode(b: var BridgeBuf; n: PNode)
proc encodeSym(b: var BridgeBuf; n: PNode) =
## `(nflags <flags> (ht <type> (bsym <idx>)))`, always the full chain.
##
## The wrappers are unconditional on purpose. The `.bif` writer emits them
## only when the node differs from its symbol, which is what creates the
## `(ht . <sym>)` shape whose nil is load-bearing and whose meaning depends on
## whether the symbol was loaded yet — a real ambiguity that cost a reverted
## commit on this branch. A bridge has no reason to inherit it: spelling the
## node's own type and flags out every time costs four tokens and makes the
## answer exact by construction.
b.bld.openTag symNodeFlagsTagName
b.emitInfo(n.info)
b.emitFlags(n.flags)
b.bld.openTag hiddenTypeTagName
b.emitTypeSlot(n.typ) # the LAZY-AWARE accessor: what `ast.typ` says
b.bld.openTag bridgeSymTagName
b.bld.addIntLit symIndex(b, n.sym).int64
b.bld.closeTag() # bsym
b.bld.closeTag() # ht
b.bld.closeTag() # nflags
proc encodeNode(b: var BridgeBuf; n: PNode) =
if n == nil:
# A nil child is a `DotToken` and has no origin: there is no node to
# remember, and `originOf` answering nil for it is the right answer.
b.bld.addDotToken()
return
# ORIGIN TRACKING. `len` is where this node's head token is about to land, and
# `cursorToPosition` is its inverse — nifcore documents that index as a stable
# key for exactly this. Recording it is what keeps `TLoc.lode` a `PNode`: a
# cursor-driven generator can still put the ORIGINAL node in a location, so
# the identity comparisons that already exist (`preventNrvo`'s `dest != le`,
# `isPartOf(d.lode, …)`) keep meaning what they meant. Without this the
# generator could not migrate without `TLoc` itself changing representation —
# and `TLoc` lives in `astdef`, at the bottom of the module graph, so that
# would push the seam far below the backend.
b.tables.origins[b.bld.buf.len] = n
if n.kind == nkSym and n.sym != nil:
encodeSym(b, n)
return
b.bld.openTag toNifTag(n.kind)
b.emitInfo(n.info)
b.emitFlags(n.flags)
b.emitTypeSlot(n.typ)
case n.kind
of nkCharLit:
b.bld.addCharLit char(n.intVal)
of nkIntLit..nkInt64Lit:
b.bld.addIntLit n.intVal
of nkUIntLit..nkUInt64Lit:
b.bld.addUIntLit cast[uint64](n.intVal)
of nkFloatLit..nkFloat128Lit:
b.bld.addFloatLit n.floatVal
of nkStrLit..nkTripleStrLit:
b.bld.addStrLit n.strVal
of nkIdent:
b.bld.addIdent n.ident.s
of nkSym:
# `n.sym == nil`, which `encodeSym` cannot express. It is a broken node
# either way; encode it as a childless `nkSym` so the walk stays total.
discard
of nkNone, nkEmpty, nkNilLit, nkType, nkCommentStmt:
discard
else:
for child in sons(n): encodeNode(b, child)
b.bld.closeTag()
proc toTokenBuf*(n: PNode; conf: ConfigRef): BridgeBuf =
## Encode a whole tree. `n` is not modified and not retained: the buffer holds
## tokens, and the tables hold the `PSym`/`PType` objects the tree pointed at.
result = initBridgeBuf(conf)
encodeNode(result, n)
# The tables carry a BORROWED pointer to the buffer so `originAt` can key
# against it. Set once, here, after encoding is finished and the buffer will
# not be reallocated out from under it.
result.tables.buf = addr result.bld.buf
proc originOf*(b: var BridgeBuf; c: Cursor): PNode {.inline.} =
## The `PNode` that was encoded at `c`, or nil when `c` is a `DotToken` (a nil
## child) or does not point at a node head. Identity-preserving: this is the
## very object the encoder was handed, not a copy, which is the whole point.
b.tables.buf = addr b.bld.buf
originAt(b.tables, c)
proc rootCursor*(b: var BridgeBuf): Cursor {.inline.} =
## A read cursor at the encoded root. `beginRead` asserts every tag was
## closed, so a mis-nested encode is caught here rather than as nonsense
## further along.
beginRead(b.bld.buf)
# ---------------------------------------------------------------------------
# Decode
#
# The other direction, for a rewriting pass that has a cursor and needs a tree
# it can mutate. Deliberately NOT written against `bnode`: this module is below
# it (`bnode` reads through a nav, which is exactly the state a decoder should
# not need), and the shape is the encoder's, right here, so the two stay
# legible as a pair.
proc decodeNode(b: BridgeBuf; c: var Cursor): PNode
proc decodeTypeSlot(b: BridgeBuf; c: var Cursor): PType =
if nifcore.kind(c) == DotToken:
result = nil
skip c
else:
doAssert nifcore.kind(c) == TagLit and
c.tags.tagName(cursorTagId(c)) == bridgeTypeTagName,
"bridge: type slot expected"
let payload = childCursor(c)
doAssert nifcore.kind(payload) == IntLit, "bridge: (btyp) payload expected"
let idx = int(nifcore.intVal(payload))
doAssert idx < b.tables.types.len, "bridge: type index out of range"
result = b.tables.types[idx]
skip c
proc decodeFlags(c: var Cursor): TNodeFlags =
result = nodeFlagsFromCursor(c)
skip c
proc decodeSym(b: BridgeBuf; c: var Cursor): PNode =
## Unwinds exactly what `encodeSym` wrote.
var outer = childCursor(c) # inside (nflags
let flags = decodeFlags(outer)
doAssert nifcore.kind(outer) == TagLit and
outer.tags.tagName(cursorTagId(outer)) == hiddenTypeTagName,
"bridge: (ht) expected inside (nflags)"
var ht = childCursor(outer) # inside (ht
let typ = decodeTypeSlot(b, ht)
doAssert nifcore.kind(ht) == TagLit and
ht.tags.tagName(cursorTagId(ht)) == bridgeSymTagName,
"bridge: (bsym) expected inside (ht)"
let payload = childCursor(ht)
doAssert nifcore.kind(payload) == IntLit, "bridge: (bsym) payload expected"
let idx = int(nifcore.intVal(payload))
doAssert idx < b.tables.syms.len, "bridge: sym index out of range"
result = newSymNode(b.tables.syms[idx], lineInfoFromCursor(program, c))
result.typField = typ
result.flags = flags
skip c
proc decodeNode(b: BridgeBuf; c: var Cursor): PNode =
case nifcore.kind(c)
of DotToken:
result = nil
skip c
of TagLit:
let tag = c.tags.tagName(cursorTagId(c))
if tag == symNodeFlagsTagName:
return decodeSym(b, c)
let kind = parse(TNodeKind, tag)
let info = lineInfoFromCursor(program, c)
var inner = childCursor(c)
let flags = decodeFlags(inner)
let typ = decodeTypeSlot(b, inner)
result = newNodeI(kind, info)
result.flags = flags
result.typField = typ
case kind
of nkCharLit..nkUInt64Lit:
result.intVal =
case nifcore.kind(inner)
of CharLit: BiggestInt(ord(charLit(inner)))
of UIntLit: cast[BiggestInt](nifcore.uintVal(inner))
else: BiggestInt(nifcore.intVal(inner))
of nkFloatLit..nkFloat128Lit:
result.floatVal = nifcore.floatVal(inner)
of nkStrLit..nkTripleStrLit:
result.strVal = strVal(inner)
of nkIdent:
result.ident = identFromCursor(program, inner)
else:
while inner.hasMore:
result.sons.add decodeNode(b, inner)
skip c
else:
raiseAssert "bridge: unexpected token " & $nifcore.kind(c)
proc toPNode*(b: var BridgeBuf): PNode =
## The tree the buffer encodes, as fresh `PNode`s sharing the ORIGINAL
## `PSym`s and `PType`s. Round-tripping is therefore identity-preserving for
## symbols and types and structure-preserving for everything else, which is
## what a rewriting pass needs: it can rebuild a subtree without the symbols
## underneath it changing identity.
var c = rootCursor(b)
result = decodeNode(b, c)

View File

@@ -29,7 +29,7 @@ const
nimEnableCovariance* = defined(nimEnableCovariance)
icFormatVersion* = "30"
icFormatVersion* = "38"
## Version of the IC cache format (the sem-NIF module layout written by
## ast2nif.nim plus the iface/impl/edges side files). Bump it whenever
## that layout changes: `commandIc` wipes a nimcache whose `ic.version`
@@ -54,6 +54,16 @@ const
## id, so its hash is stable across the NIF boundary (was breaking
## nim-serialization's auto-serialization lookup under IC). The sem-NIF
## macrocache entries and baked generic-instance bodies hold the old hashes.
## v7 (=31): anonymous wrapper types (`var T`, `lent T`, `sink T`, tuples)
## are named by their CONTENT instead of `itemId.item`, the module-wide
## type-mint counter (see ast2nif.CanonTypeKinds). Old caches name the same
## type differently, so every `.s.bif` reference would dangle.
## v8 (=32): the same for `tyProc`, except that a proc type which is a
## routine's SIGNATURE is named after that routine rather than by content
## (see ast2nif.sigRoutineOf). Renames types, so old caches dangle again.
## v9 (=33): and for the per-module `int`/`float` LITERAL COPIES (see
## ast2nif.CanonLitCopyKinds), the last mover that broke a build outright
## (`symbol has no offset` out of a cached `.t.bif`). Renames types again.
type # please make sure we have under 32 options
# (improves code efficiency a lot!)
@@ -458,10 +468,16 @@ type
# codegen+DCE+cc+link in one process). The stages
# are wired as nifmake rules by `deps.nim`'s backend
# build file. See `compiler/nifbackend.nim`.
icBackendModule*: string # under `nim nifc` with icBackendStage in {cg,emit}:
# the NIF module suffix this invocation codegens or
# emits. The other modules are loaded only so types
# resolve; their definitions are referenced extern.
icBackendModules*: seq[string]
# under `nim nifc` with icBackendStage in
# {lower,cg,emit}: the NIF module suffixes this
# invocation processes — its BATCH. One entry is
# the per-module fan-out; several share one process
# and therefore ONE dependency-closure load between
# them, which is the whole point (see
# `nifbackend.loadDepClosure`). Every other module
# is loaded only so types resolve; its definitions
# are referenced extern. Empty = the main module.
spellSuggestMax*: int # max number of spelling suggestions for typos
cppDefines*: HashSet[string] # (*)
@@ -930,6 +946,24 @@ proc getOsCacheDir(): string =
else:
result = getHomeDir() / genSubDir.string
proc isIcDriver*(conf: ConfigRef): bool =
## True for `nim c --ic:on` / `nim cpp --ic:on`: this process is the `nim ic`
## DRIVER (it builds the nifmake graph and spawns the per-module children),
## not a compilation. `nim ic` itself keeps its own `cmdIc` branch.
conf.ic and conf.cmd in {cmdCompileToC, cmdCompileToCpp, cmdCompileToOC}
proc icCFileExt*(conf: ConfigRef): string =
## The extension the per-module backend gives a module's translation unit.
## Mirrors `cgen.getCFile` at BACKEND granularity, which is all the `nim ic`
## driver can know: it DECLARES every module's `.c`/`.cpp` output to nifmake
## without loading a single module, so a per-module `{.compile: cpp.}`
## (`sfCompileToCpp`) is out of reach — and `nim cpp` selects the backend for
## the whole program anyway.
case conf.backend
of backendCpp: ".nim.cpp"
of backendObjc: ".nim.m"
else: ".nim.c"
proc getNimcacheDir*(conf: ConfigRef): AbsoluteDir =
proc nimcacheSuffix(conf: ConfigRef): string =
if conf.ideActive: "_nimsuggest" # dedicated cache, never shared with `nim c`

View File

@@ -6,9 +6,11 @@ import sem, cgen, modulegraphs, ast, llstream, parser, msgs,
when not defined(nimKochBootstrap):
import vmdef
import ast2nif
import "../dist/nimony/src/lib" / [nifstreams, bitabs]
import nifstreams
import "../dist/nimony/src/lib" / bitabs
import pipelineutils
import icprof
import ../dist/checksums/src/checksums/sha1
@@ -248,7 +250,15 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
# current strongly-connected import group (`--icGroup`) are the exception:
# they are compiled from source here, so each must write its own NIF.
let shouldWriteNif =
if graph.config.ideActive:
if graph.config.errorCounter > 0:
# Never persist an artifact built from erroneous AST. `nim m` does exit
# non-zero, but its outputs would still land on disk NEWER than their
# inputs, so nifmake sees the rule as satisfied on the next run: the
# build then "succeeds" from a poisoned NIF — a silently wrong binary,
# or an internal error once codegen meets an `nkError` body. Leaving the
# outputs missing keeps the rule dirty so it re-fires and re-reports.
false
elif graph.config.ideActive:
# nimsuggest (cmdM): persist NIF for cleanly-compiled, SAVED modules so
# later queries load them instead of recompiling. Never persist the
# actively edited buffer (it may hold unsaved/incomplete code) nor a
@@ -305,7 +315,7 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
var typeOffers: seq[tuple[generic: PSym; inst: PType]] = @[]
for genItemId, instList in graph.typeInstCache:
for inst in instList:
if inst != nil and inst.uniqueId.module == module.position and
if inst != nil and inst.itemId.module == module.position and
inst.kidsLen > 0 and inst[0] != nil and
inst[0].kind == tyGenericBody and inst[0].sym != nil:
typeOffers.add (inst[0].sym, inst)
@@ -320,10 +330,19 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
let firstUnusedId = max(idgen.symId, idgen.typeId)
var expansions: seq[(PSym, TLineInfo)] = @[]
discard graph.nifExpansions.take(module.position.int32, expansions)
writeNifModule(graph.config, module.position.int32, topLevelStmts, graph.opsLog,
replayActions, implDeps, reexportedModuleSyms(graph, module),
genericOffers, typeOffers, resolvedImportDeps, firstUnusedId,
expansions)
# The module symbol's own backend-relevant flags. `sfInjectDestructors` is
# set by sempass2 when the module's TOP-LEVEL statements need the
# destructor pass; `moduleFromNifFile` builds a fresh module PSym, so
# without persisting it `cgen.genTopLevelStmt` skipped
# `injectDestructorCalls` and top-level locals were never destroyed.
let moduleFlags =
if sfInjectDestructors in module.flags: ModFlagInjectDestructors else: 0'i32
timed tWriteNif:
writeNifModule(graph.config, module.position.int32, topLevelStmts, graph.opsLog,
replayActions, implDeps, reexportedModuleSyms(graph, module),
genericOffers, typeOffers, resolvedImportDeps, firstUnusedId,
expansions, moduleFlags,
reexportedLocalSyms(graph, module))
# The module's REAL direct imports (incl. macro-generated) for `nim ic`'s
# graph re-derivation; see ast2nif.writeSemDeps / semdata.addImportFileDep.
var semDepPaths: seq[string] = @[]

View File

@@ -77,7 +77,7 @@ proc isAttachableRoutineTo(prc: PSym, arg: PType): bool =
# has default value, parameter is not considered in type attachment
continue
let t = nominalRoot(prc.typ[i])
if t != nil and t.itemId == arg.itemId:
if t != nil and t.bindingId == arg.bindingId:
# parameter `i` is a nominal type in this module
# attachable if the nominal root `t` has the same id as `arg`
return true
@@ -735,10 +735,10 @@ proc indexTypesMatch(c: PContext, f, a: PType, arg: PNode): PNode =
when defined(icDbg):
if result == nil and f != nil and a != nil and f.kind == tyEnum:
echo "INDEXMISMATCH f=", typeToString(f), " itemId=", f.itemId,
" uniqueId=", f.uniqueId, " mod=", toFullPath(c.config, f.itemId.module.FileIndex),
" bindingId=", f.bindingId, " mod=", toFullPath(c.config, f.itemId.module.FileIndex),
" sym=", (if f.sym != nil: $f.sym.itemId else: "nil"), " state=", f.state
let a2 = a.skipTypes({tyRange})
echo " a=", typeToString(a), " itemId=", a2.itemId, " uniqueId=", a2.uniqueId,
echo " a=", typeToString(a), " itemId=", a2.itemId, " bindingId=", a2.bindingId,
" mod=", toFullPath(c.config, a2.itemId.module.FileIndex),
" sym=", (if a2.sym != nil: $a2.sym.itemId else: "nil"), " state=", a2.state

View File

@@ -1931,7 +1931,7 @@ proc borrowCheck(c: PContext, n, le, ri: PNode) =
PathKinds0 = {nkDotExpr, nkCheckedFieldExpr,
nkBracketExpr, nkAddr, nkHiddenAddr,
nkObjDownConv, nkObjUpConv}
PathKinds1 = {nkHiddenStdConv, nkHiddenSubConv}
PathKinds1 = {nkHiddenStdConv, nkHiddenSubConv, nkCast}
proc getRoot(n: PNode; followDeref: bool): PNode =
result = n
@@ -2187,7 +2187,7 @@ proc semProcBody(c: PContext, n: PNode; expectedType: PType = nil): PNode =
echo "[icMetaRet] meta result type for ", c.p.owner.name.s, ": ",
typeToString(c.p.resultSym.typ), " kind=", c.p.resultSym.typ.kind,
" flags=", c.p.resultSym.typ.flags,
" uid=", c.p.resultSym.typ.uniqueId.module, ".", c.p.resultSym.typ.uniqueId.item,
" itemId=", c.p.resultSym.typ.itemId.module, ".", c.p.resultSym.typ.itemId.item,
" state=", c.p.resultSym.typ.state
if isEmptyType(result.typ):
# we inferred a 'void' return type:

View File

@@ -129,7 +129,12 @@ proc semGenericStmtSymbol(c: PContext, n: PNode, s: PSym,
result.typ = nil
onUse(n.info, s)
of skParam:
if s.owner == c.p.owner:
if s.typ != nil and s.typ.kind == tyStatic and s.typ.n != nil:
# The enclosing routine gives this static parameter a concrete value.
# Keep that value so the nested generic can fold it as a compile-time
# expression instead of generating a runtime parameter reference.
result = s.typ.n
elif s.owner == c.p.owner:
# Parameters of the routine currently being semchecked stay as local
# identifiers
result = n
@@ -681,4 +686,3 @@ proc semConceptBody(c: PContext, n: PNode): PNode =
)
result = semGenericStmt(c, n, {withinConcept}, ctx)
semIdeForTemplateOrGeneric(c, result, ctx.cursorInBody)

View File

@@ -349,7 +349,7 @@ proc instantiateProcType(c: PContext, pt: LayeredIdTable,
when defined(icDbgRefc):
echo "[icInst] ", prc.name.s, " param ", oldParam.name.s,
": ", typeToString(resulti), " (kind=", resulti.kind,
" uid=", resulti.uniqueId.module, ".", resulti.uniqueId.item,
" itemId=", resulti.itemId.module, ".", resulti.itemId.item,
" flags=", resulti.flags, ") -> ", typeToString(paramType),
" (kind=", paramType.kind, ")"
@@ -407,6 +407,10 @@ proc instantiateProcType(c: PContext, pt: LayeredIdTable,
eraseVoidParams(result)
skipIntLiteralParams(result, c.idgen)
# The signature belongs to the INSTANCE, not to the generic it was copied
# from: `instCopyType` above kept the generic's owner, and every parameter has
# already been re-owned with `setOwner(param, prc)`.
setOwner(result, prc)
prc.typ = result
popInfoContext(c.config)

View File

@@ -478,6 +478,15 @@ proc turnFinalizerIntoDestructor(c: PContext; orig: PSym; info: TLineInfo): PSym
# proc signature:
result.typ = newProcType(result.info, c.idgen, result)
result.typ.addParam newParam
# `transform` only rewrites the PARAMETER, so the copied AST still names `orig`
# at `namePos`. Make the definition name itself, the invariant every other
# routine AST keeps: the NIF writer re-derives a routine's serialized AST from
# `ast[namePos].sym.ast` (ast2nif's `nkProcDef` branch), so a stale name node
# made this proc serialize `orig`'s body — whose parameter belongs to `orig`.
# Lambda lifting then saw the body's parameter as a variable captured from
# another proc and aborted with "internal error: environment misses: x".
if result.ast != nil and result.ast.safeLen > namePos:
result.ast[namePos] = newSymNode(result, result.info)
proc semQuantifier(c: PContext; n: PNode): PNode =
checkSonsLen(n, 2, c.config)

View File

@@ -109,7 +109,7 @@ proc getObjDepth(t: PType): (int, ItemId) =
x = skipTypes(x, skipPtrs)
if x.kind != tyObject:
return (-3, default(ItemId))
stack.add x.itemId
stack.add x.bindingId
x = x.baseClass
inc(result[0])
result[1] = stack[^2]

View File

@@ -1819,7 +1819,7 @@ proc typeSectionFinalPass(c: PContext, n: PNode) =
var reified = semTypeNode(c, typeNode, nil)
assert reified != nil
assignType(typ, reified)
typ.itemId = reified.itemId # same id
typ.bindingId = reified.bindingId # same id
if containsForwardType(typ):
c.forwardTypeUpdates.add (owner, typ, typeNode)
elif not remainingOwners.missingOrExcl(owner.id):
@@ -2160,47 +2160,54 @@ proc checkedForDestructor(t: PType): bool =
return true
result = false
proc whereToBindTypeHook(c: PContext; t: PType): PType =
proc normalizeTypeHook(t: PType; markAsgn = false): PType =
result = t
while true:
if result.kind in {tyGenericBody, tyGenericInst}: result = result.skipModifier
elif result.kind == tyGenericInvocation: result = result[0]
else: break
if markAsgn:
incl(result, tfHasAsgn)
if result.kind == tyCompositeTypeClass and result.base.kind == tyGenericBody:
result = result.base
elif result.kind in {tyGenericBody, tyGenericInst}:
result = result.skipModifier
elif result.kind == tyGenericInvocation:
result = result.genericHead
else:
break
proc whereToBindTypeHook(c: PContext; t: PType): PType =
result = normalizeTypeHook(t)
if result.kind in {tyObject, tyDistinct, tySequence, tyString}:
result = canonType(c, result)
proc bindHookToType(c: PContext; s: PSym; n: PNode; op: TTypeAttachedOp;
typeToBind: PType): bool =
var obj = typeToBind
if obj.kind notin {tyObject, tyDistinct, tySequence, tyString}:
return false
obj = canonType(c, obj)
let ao = getAttachedOp(c.graph, obj, op)
if ao == s:
discard "forward declared hook"
elif ao.isNil and not checkedForDestructor(obj):
setAttachedOp(c.graph, c.module.position, obj, op, s)
else:
prevDestructor(c, op, ao, obj, n.info)
if obj.owner.getModule != s.getModule:
localError(c.config, n.info, errGenerated,
"type bound operation `" & s.name.s & "` can be defined only in the same module with its type (" & obj.typeToString() & ")")
result = true
proc bindDupHook(c: PContext; s: PSym; n: PNode; op: TTypeAttachedOp) =
let t = s.typ
var noError = false
let cond = t.len == 2 and t.returnType != nil
if cond:
var obj = t.firstParamType
while true:
incl(obj, tfHasAsgn)
if obj.kind in {tyGenericBody, tyGenericInst}: obj = obj.skipModifier
elif obj.kind == tyGenericInvocation: obj = obj.genericHead
else: break
var obj = normalizeTypeHook(t.firstParamType, markAsgn = true)
let res = normalizeTypeHook(t.returnType)
var res = t.returnType
while true:
if res.kind in {tyGenericBody, tyGenericInst}: res = res.skipModifier
elif res.kind == tyGenericInvocation: res = res.genericHead
else: break
if obj.kind in {tyObject, tyDistinct, tySequence, tyString} and sameType(obj, res):
obj = canonType(c, obj)
let ao = getAttachedOp(c.graph, obj, op)
if ao == s:
discard "forward declared destructor"
elif ao.isNil and not checkedForDestructor(obj):
setAttachedOp(c.graph, c.module.position, obj, op, s)
else:
prevDestructor(c, op, ao, obj, n.info)
noError = true
if obj.owner.getModule != s.getModule:
localError(c.config, n.info, errGenerated,
"type bound operation `" & s.name.s & "` can be defined only in the same module with its type (" & obj.typeToString() & ")")
if sameType(obj, res):
noError = bindHookToType(c, s, n, op, obj)
if not noError and sfSystemModule notin s.owner.flags:
localError(c.config, n.info, errGenerated,
@@ -2230,25 +2237,8 @@ proc bindTypeHook(c: PContext; s: PSym; n: PNode; op: TTypeAttachedOp) =
t.len >= 2 and t.returnType == nil
if cond:
var obj = t.firstParamType.skipTypes({tyVar})
while true:
incl(obj, tfHasAsgn)
if obj.kind in {tyGenericBody, tyGenericInst}: obj = obj.skipModifier
elif obj.kind == tyGenericInvocation: obj = obj.genericHead
else: break
if obj.kind in {tyObject, tyDistinct, tySequence, tyString}:
obj = canonType(c, obj)
let ao = getAttachedOp(c.graph, obj, op)
if ao == s:
discard "forward declared destructor"
elif ao.isNil and not checkedForDestructor(obj):
setAttachedOp(c.graph, c.module.position, obj, op, s)
else:
prevDestructor(c, op, ao, obj, n.info)
noError = true
if obj.owner.getModule != s.getModule:
localError(c.config, n.info, errGenerated,
"type bound operation `" & s.name.s & "` can be defined only in the same module with its type (" & obj.typeToString() & ")")
var obj = normalizeTypeHook(t.firstParamType.skipTypes({tyVar}), markAsgn = true)
noError = bindHookToType(c, s, n, op, obj)
if not noError and sfSystemModule notin s.owner.flags:
case op
of attachedTrace:
@@ -2315,35 +2305,12 @@ proc semOverride(c: PContext, s: PSym, n: PNode) =
message(c.config, n.info, warnDeprecated, "Overriding `=` hook is deprecated; Override `=copy` hook instead")
let t = s.typ
if t.len == 3 and t.returnType == nil and t.firstParamType.kind == tyVar:
var obj = t.firstParamType.elementType
while true:
incl(obj, tfHasAsgn)
if obj.kind == tyGenericBody: obj = obj.skipModifier
elif obj.kind == tyGenericInvocation: obj = obj.genericHead
else: break
var objB = t[2]
while true:
if objB.kind == tyGenericBody: objB = objB.skipModifier
elif objB.kind in {tyGenericInvocation, tyGenericInst}:
objB = objB.genericHead
else: break
if obj.kind in {tyObject, tyDistinct, tySequence, tyString} and sameType(obj, objB):
var obj = normalizeTypeHook(t.firstParamType.elementType, markAsgn = true)
let objB = normalizeTypeHook(t[2])
if sameType(obj, objB):
# attach these ops to the canonical tySequence
obj = canonType(c, obj)
#echo "ATTACHING TO ", obj.id, " ", s.name.s, " ", cast[int](obj)
let k = if name == "=" or name == "=copy": attachedAsgn else: attachedSink
let ao = getAttachedOp(c.graph, obj, k)
if ao == s:
discard "forward declared op"
elif ao.isNil and not checkedForDestructor(obj):
setAttachedOp(c.graph, c.module.position, obj, k, s)
else:
prevDestructor(c, k, ao, obj, n.info)
if obj.owner.getModule != s.getModule:
localError(c.config, n.info, errGenerated,
"type bound operation `" & name & "` can be defined only in the same module with its type (" & obj.typeToString() & ")")
return
if bindHookToType(c, s, n, k, obj): return
if sfSystemModule notin s.owner.flags:
localError(c.config, n.info, errGenerated,
"signature for '" & s.name.s & "' must be proc[T: object](x: var T; y: T)")
@@ -2409,7 +2376,8 @@ proc semCppMember(c: PContext; s: PSym; n: PNode) =
if typ.kind != tyObject:
localError(c.config, n.info, pragmaName & " must be either ptr to object or object type.")
if sameOwners(typ.owner, s.owner) and sameOwners(c.module, s.owner):
c.graph.memberProcsPerType.mgetOrPut(typ.itemId, @[]).add s
c.graph.memberProcsPerType.mgetOrPut(typ.bindingId, @[]).add s
logCppMember(c.graph, s)
else:
localError(c.config, n.info,
pragmaName & " procs must be defined in the same scope as the type they are virtual for and it must be a top level scope")
@@ -2417,7 +2385,7 @@ proc semCppMember(c: PContext; s: PSym; n: PNode) =
localError(c.config, n.info, pragmaName & " procs are only supported in C++")
else:
var typ = s.typ.returnType
if typ != nil and typ.kind == tyObject and typ.itemId notin c.graph.initializersPerType:
if typ != nil and typ.kind == tyObject and typ.bindingId notin c.graph.initializersPerType:
var initializerCall = newTree(nkCall, newSymNode(s))
var isInitializer = n[paramsPos].len > 1
for i in 1..<n[paramsPos].len:
@@ -2431,7 +2399,8 @@ proc semCppMember(c: PContext; s: PSym; n: PNode) =
initializerCall.add val
inc j
if isInitializer:
c.graph.initializersPerType[typ.itemId] = initializerCall
c.graph.initializersPerType[typ.bindingId] = initializerCall
logCppMember(c.graph, s)
proc semMethodPrototype(c: PContext; s: PSym; n: PNode) =
if s.isGenericRoutine:

View File

@@ -1379,7 +1379,7 @@ proc liftParamType(c: PContext, procKind: TSymKind, genericParams: PNode,
for i in 0..<paramType.len - 1:
if paramType[i].kind == tyStatic:
var staticCopy = paramType[i].exactReplica(c.idgen)
var staticCopy = copyType(paramType[i], c.idgen, paramType[i].owner)
staticCopy.incl tfInferrableStatic
result.rawAddSon staticCopy
else:
@@ -2481,7 +2481,7 @@ proc semTypeNode(c: PContext, n: PNode, prev: PType): PType =
# bugfix: keep the fresh id for aliases to integral types:
if s.typ.kind notin {tyBool, tyChar, tyInt..tyInt64, tyFloat..tyFloat128,
tyUInt..tyUInt64}:
prev.itemId = s.typ.itemId
prev.bindingId = s.typ.bindingId
result = prev
of nkSym:
let s = getGenSym(c, n.sym)

View File

@@ -376,8 +376,8 @@ proc lookupTypeVar(cl: var TReplTypeVars, t: PType): PType =
result = cl.typeMap.lookup(t)
when defined(icDbgRefc):
if t.kind in {tyGenericParam, tyTypeDesc}:
echo "[icBind] lookup ", t.kind, " ", typeToString(t), " uid=", t.uniqueId.module, ".",
t.uniqueId.item, " itemId=", t.itemId.module, ".", t.itemId.item,
echo "[icBind] lookup ", t.kind, " ", typeToString(t), " itemId=", t.itemId.module, ".",
t.itemId.item, " bindingId=", t.bindingId.module, ".", t.bindingId.item,
" state=", t.state, " flags=", t.flags, " -> ",
(if result != nil: typeToString(result) else: "MISS"),
" allowMeta=", cl.allowMetaTypes
@@ -423,7 +423,7 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
var header = t
# search for some instantiation here:
if cl.allowMetaTypes:
result = getOrDefault(cl.localCache, t.itemId)
result = getOrDefault(cl.localCache, t.bindingId)
else:
result = searchInstTypes(cl.c.graph, t)
@@ -473,7 +473,7 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
if not cl.allowMetaTypes:
cacheTypeInst(cl.c, result)
else:
cl.localCache[t.itemId] = result
cl.localCache[t.bindingId] = result
let oldSkipTypedesc = cl.skipTypedesc
cl.skipTypedesc = true
@@ -647,7 +647,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
# type
# Vector[N: static[int]] = array[N, float64]
# TwoVectors[Na, Nb: static[int]] = (Vector[Na], Vector[Nb])
result = getOrDefault(cl.localCache, t.itemId)
result = getOrDefault(cl.localCache, t.bindingId)
if result != nil: return result
inc cl.recursionLimit
@@ -739,7 +739,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
return
bailout()
result = instCopyType(cl, t)
cl.localCache[t.itemId] = result
cl.localCache[t.bindingId] = result
for i in FirstGenericParamAt..<result.kidsLen:
var r = result[i]
if r != nil:
@@ -755,7 +755,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
of tyGenericInst, tyUserTypeClassInst:
bailout()
result = instCopyType(cl, t)
cl.localCache[t.itemId] = result
cl.localCache[t.bindingId] = result
for i in FirstGenericParamAt..<result.kidsLen:
result[i] = replaceTypeVarsT(cl, result[i])
propagateToOwner(result, result.last)
@@ -770,7 +770,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
result = instCopyType(cl, t)
result.size = -1 # needs to be recomputed
#if not cl.allowMetaTypes:
cl.localCache[t.itemId] = result
cl.localCache[t.bindingId] = result
let propagateInstValue = isInstValue and isRefPtrObject(t)
for i, resulti in result.ikids:
@@ -819,7 +819,7 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
result = t
# Slow path, we have some work to do. CRUCIAL: only ever mutate a type that
# is LOCAL to the module we are instantiating in (`uniqueId.module ==
# is LOCAL to the module we are instantiating in (`itemId.module ==
# idgen.module`). A type loaded from another module's NIF (foreign) already
# had its object branches resolved when it was originally compiled; mutating
# it in place here is an old→new heap write that re-homes the loaded type to
@@ -828,11 +828,11 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
# prior `state != Sealed` guard was insufficient: a freshly-LOADED type is
# `Complete`, not `Sealed` (`Sealed` only means "already re-written to a NIF").
if t.kind == tyRef and t.hasElementType and t.elementType.kind == tyObject and
t.elementType.n != nil and t.elementType.uniqueId.module == cl.c.idgen.module.int:
t.elementType.n != nil and t.elementType.itemId.module == cl.c.idgen.module.int:
discard replaceObjBranches(cl, t.elementType.n)
elif result.n != nil and t.kind == tyObject and result.state != Sealed and
result.uniqueId.module == cl.c.idgen.module.int:
result.itemId.module == cl.c.idgen.module.int:
# Invalidate the type size as we may alter its structure
result.size = -1
result.n = replaceObjBranches(cl, result.n)

View File

@@ -150,7 +150,7 @@ proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]; conf: Confi
if hashDepth > hashMaxDepth: hashMaxDepth = hashDepth
if hashCalls >= 500_000_000 and hashCalls <= 500_000_300:
echo "HASHLOOP n=", hashCalls, " d=", hashDepth, " kind=", t.kind, " id=", t.itemId,
" uniq=", t.uniqueId, " sym=", (if t.sym != nil: t.sym.name.s else: "NIL"),
" bindingId=", t.bindingId, " sym=", (if t.sym != nil: t.sym.name.s else: "NIL"),
" state=", t.state, " owner=", (if t.owner != nil: t.owner.name.s else: "NIL")
elif hashCalls == 500_000_301:
echo "HASHLOOP maxDepth=", hashMaxDepth
@@ -209,7 +209,11 @@ proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]; conf: Confi
# backend spelling instead of collapsing into the generic Nim builtin:
c &= char(t.kind)
if t.sym != nil and {sfImportc, sfExportc} * t.sym.flags != {}:
c.hashSym(t.sym)
# Aliases inherit the external name, but have a different symbol.
if t.sym.loc.snippet != "":
c &= t.sym.loc.snippet
else:
c.hashSym(t.sym)
of tyObject, tyEnum:
if t.typeInstImpl != nil:
# prevent against infinite recursions here, see bug #8883:

View File

@@ -137,8 +137,8 @@ proc put(c: var TCandidate, key, val: PType) {.inline.} =
echo "binding ", key, " -> ", val
when defined(icDbgRefc):
if key.kind in {tyGenericParam, tyTypeDesc}:
echo "[icBind] put ", key.kind, " ", typeToString(key), " uid=", key.uniqueId.module, ".",
key.uniqueId.item, " itemId=", key.itemId.module, ".", key.itemId.item,
echo "[icBind] put ", key.kind, " ", typeToString(key), " itemId=", key.itemId.module, ".",
key.itemId.item, " bindingId=", key.bindingId.module, ".", key.bindingId.item,
" state=", key.state, " -> ", typeToString(val)
put(c.bindings, key, val.skipIntLit(c.c.idgen))
@@ -913,16 +913,14 @@ proc matchUserTypeClass*(m: var TCandidate; ff, a: PType): PType =
case typ.kind
of tyStatic:
param = paramSym skConst
param.typ = typ.exactReplica(m.c.idgen)
#copyType(typ, c.idgen, typ.owner)
param.typ = copyType(typ, m.c.idgen, typ.owner)
if typ.n == nil:
param.typ.incl tfInferrableStatic
else:
param.ast = typ.n
of tyFromExpr:
param = paramSym skVar
param.typ = typ.exactReplica(m.c.idgen)
#copyType(typ, c.idgen, typ.owner)
param.typ = copyType(typ, m.c.idgen, typ.owner)
else:
param = paramSym skType
param.typ = if typ.isMetaType:
@@ -974,8 +972,7 @@ proc matchUserTypeClass*(m: var TCandidate; ff, a: PType): PType =
if ff.kind == tyUserTypeClassInst:
result = generateTypeInstance(c, m.bindings, typeClass.sym.info, ff)
else:
result = ff.exactReplica(m.c.idgen)
#copyType(ff, c.idgen, ff.owner)
result = copyType(ff, m.c.idgen, ff.owner)
result.n = checkedBody
@@ -1169,6 +1166,8 @@ proc enterConceptMatch(c: var TCandidate; f,a: PType, flags: TTypeRelFlags): TTy
return typeRel(c, prev, a, flags)
if trDontBind in flags:
conceptFlags.incl mfDontBind
if trBindGenericParam in flags:
conceptFlags.incl mfBindGenericParam
if trCheckGeneric in flags:
conceptFlags.incl mfCheckGeneric
let mres = concepts.conceptMatch(c.c, concpt, a, c.bindings, container, flags = conceptFlags)
@@ -1239,11 +1238,17 @@ proc typeRel(c: var TCandidate, f, aOrig: PType,
tfConceptMatchedTypeSym notin aOrig.flags
template skipTypeCursor(it, kinds: untyped) =
# `ast.last`, not a hand-inlined copy of it. What this replaces was `last`'s
# body verbatim MINUS its `if state == Partial: loadType` line -- and that
# line is the whole point: a NIF-loaded stub answers `kind` off its NIF name
# while `sonsImpl` is still EMPTY, so `sonsImpl[^1]` raised IndexDefect.
# nimbus-eth2 died on it in the very first `nim ic` pass, inside the `x is T`
# under a chronos `{.async.}` iterator's `when`. The second call site below
# is unguarded and runs on EVERY `typeRel`, so this is not a concept-only
# corner: a probe counts 195 Partial `tyVar`/`tyLent` arrivals across one
# nimbus frontend, each of which was an IndexDefect waiting for its turn.
while it.kind in kinds:
if it.kind == tyProc and it.nImpl.len > 1:
it = it.nImpl[^1].sym.typ
else:
it = it.sonsImpl[^1]
it = it.last
var aOrig {.cursor.} = aOrig
if useTypeLoweringRuleInTypeClass:
@@ -2689,7 +2694,7 @@ proc staticAwareTypeRel(m: var TCandidate, f: PType, arg: var PNode): TTypeRelat
# The ast of the type does not point to the symbol.
# Without this we will never resolve a `static proc` with overloads
let copiedNode = copyNode(arg)
copiedNode.typ = exactReplica(copiedNode.typ, m.c.idgen)
copiedNode.typ = copyType(copiedNode.typ, m.c.idgen, copiedNode.typ.owner)
copiedNode.typ.n = arg
arg = copiedNode
typeRel(m, f, arg.typ)

View File

@@ -38,6 +38,15 @@ proc transformBody*(g: ModuleGraph; idgen: IdGenerator; prc: PSym; flags: Transf
import closureiters, lambdalifting
when not defined(nimKochBootstrap):
# The `PNode` -> `TokenBuf` bridge, and through it `bodynav`, which resolves
# names against `ast.program`. `program` does not EXIST under
# `-d:nimKochBootstrap` — that define disables the whole IC subsystem (see
# `ast.nim` and `koch.bootic`) — so the bridge has to be out of that build
# too, not merely unused by it. `handOffBody` below is guarded for the same
# reason; its only caller is `cgen`, under `-d:newIcBackend`.
import nodebridge
type
PTransCon = ref object # part of TContext; stackable
mapping: TIdTable[PNode] # mapping from symbols to nodes
@@ -1436,6 +1445,25 @@ proc transformBody*(g: ModuleGraph; idgen: IdGenerator; prc: PSym; flags: Transf
#if prc.name.s == "main":
# echo "transformed into ", renderTree(result, {renderIds})
when not defined(nimKochBootstrap):
proc handOffBody*(body: PNode; conf: ConfigRef): BridgeBuf =
## THE HANDOFF from the rewriting stage to the reading stage: the transformed
## body, as a `TokenBuf` a reader can cursor over (`nodebridge`).
##
## It lives here because the invariant it carries is this module's: a bridged
## buffer is a SNAPSHOT, so it must be taken after the LAST rewrite the body
## will receive. Anything that mutates a node afterwards — `cgen.easyResultAsgn`
## setting `nfPreventCg` is the one that does — leaves the buffer describing a
## tree that no longer exists.
##
## The call site is in `cgen` rather than at the end of `transformBody` for
## exactly that reason: destructor injection runs *after* `transformBody`
## returns and is another rewrite, so transforming is not the last step and a
## buffer taken here would be stale before it was read. `transformBody` returns
## a `PNode` on purpose; this is the point where a caller that has finished
## rewriting says so.
result = toTokenBuf(body, conf)
proc transformStmt*(g: ModuleGraph; idgen: IdGenerator; module: PSym, n: PNode; flags: TransformFlags = {}): PNode =
if nfTransf in n.flags:
result = n

View File

@@ -10,7 +10,7 @@
# tree helper routines
import
ast, wordrecg, idents
ast, wordrecg, idents, bnode
proc cyclicTreeAux(n: PNode, visited: var seq[PNode]): bool =
result = false
@@ -83,16 +83,17 @@ proc sameTree*(a, b: PNode): bool =
if not sameTree(a[i], b[i]): return
result = true
proc getMagic*(op: PNode): TMagic =
if op == nil: return mNone
proc getMagic*(op: AnyNode): TMagic =
if op.isNilNode: return mNone
case op.kind
of nkCallKinds:
case op[0].kind
of nkSym: result = op[0].sym.magic
let callee = op.firstSon
case callee.kind
of nkSym: result = callee.sym.magic
else: result = mNone
else: result = mNone
proc isConstExpr*(n: PNode): bool =
proc isConstExpr*(n: AnyNode): bool =
const atomKinds = {nkCharLit..nkNilLit} # Char, Int, UInt, Str, Float and Nil literals
n.kind in atomKinds or nfAllConst in n.flags
@@ -102,15 +103,16 @@ proc isCaseObj*(n: PNode): bool =
for i in 0..<n.safeLen:
if n[i].isCaseObj: return true
proc isDeepConstExpr*(n: PNode; preventInheritance = false): bool =
proc isDeepConstExpr*(n: AnyNode; preventInheritance = false): bool =
case n.kind
of nkCharLit..nkNilLit:
result = true
of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv:
result = isDeepConstExpr(n[1], preventInheritance)
result = isDeepConstExpr(n.secondSon, preventInheritance)
of nkCurly, nkBracket, nkPar, nkTupleConstr, nkObjConstr, nkClosure, nkRange:
for i in ord(n.kind == nkObjConstr)..<n.len:
if not isDeepConstExpr(n[i], preventInheritance): return false
# `nkObjConstr` carries its TYPE as child 0 and its fields from 1.
for it in sonsFrom(n, ord(n.kind == nkObjConstr)):
if not isDeepConstExpr(it, preventInheritance): return false
if n.typ.isNil: result = true
else:
let t = n.typ.skipTypes({tyGenericInst, tyDistinct, tyAlias, tySink, tyOwned})
@@ -139,17 +141,17 @@ proc isRange*(n: PNode): bool {.inline.} =
else:
result = false
proc whichPragma*(n: PNode): TSpecialWord =
let key = if n.kind in nkPragmaCallKinds and n.len > 0: n[0] else: n
proc whichPragma*(n: AnyNode): TSpecialWord =
let key = if n.kind in nkPragmaCallKinds and n.hasSons: n.firstSon else: n
case key.kind
of nkIdent: result = whichKeyword(key.ident)
of nkSym: result = whichKeyword(key.sym.name)
of nkCast: return wCast
of nkClosedSymChoice, nkOpenSymChoice, nkOpenSym:
return whichPragma(key[0])
return whichPragma(key.firstSon)
of nkBracketExpr:
if n.kind notin nkPragmaCallKinds: return wInvalid
result = whichPragma(key[0])
result = whichPragma(key.firstSon)
if result notin {wHint, wHintAsError, wWarning, wWarningAsError}:
# note bracket pragmas, see processNote
result = wInvalid
@@ -205,7 +207,7 @@ proc extractRange*(k: TNodeKind, n: PNode, a, b: int): PNode =
result = newNodeI(k, n.info, b-a+1)
for i in 0..b-a: result[i] = n[i+a]
proc getRoot*(n: PNode): PSym =
proc getRoot*(n: AnyNode): PSym =
## ``getRoot`` takes a *path* ``n``. A path is an lvalue expression
## like ``obj.x[i].y``. The *root* of a path is the symbol that can be
## determined as the owner; ``obj`` in the example.
@@ -217,11 +219,11 @@ proc getRoot*(n: PNode): PSym =
result = nil
of nkDotExpr, nkBracketExpr, nkHiddenDeref, nkDerefExpr,
nkObjUpConv, nkObjDownConv, nkCheckedFieldExpr, nkHiddenAddr, nkAddr:
result = getRoot(n[0])
result = getRoot(n.firstSon)
of nkHiddenStdConv, nkHiddenSubConv, nkConv:
result = getRoot(n[1])
result = getRoot(n.secondSon)
of nkCallKinds:
if getMagic(n) == mSlice: result = getRoot(n[1])
if getMagic(n) == mSlice: result = getRoot(n.secondSon)
else: result = nil
else: result = nil
@@ -252,8 +254,8 @@ proc isRunnableExamples*(n: PNode): bool =
result = n.kind == nkSym and n.sym.magic == mRunnableExamples or
n.kind == nkIdent and n.ident.id == ord(wRunnableExamples)
proc skipAddr*(n: PNode): PNode {.inline.} =
result = if n.kind in {nkAddr, nkHiddenAddr}: n[0] else: n
proc skipAddr*[T: AnyNode](n: T): T {.inline.} =
result = if n.kind in {nkAddr, nkHiddenAddr}: n.firstSon else: n
proc getPotentialWrites*(n: PNode; mutate: bool; result: var seq[PNode]) =
case n.kind:

View File

@@ -27,7 +27,7 @@ proc hashTree*(n: PNode): Hash =
of nkCharLit..nkUInt64Lit: result = result !& hash(n.intVal)
of nkFloatLit..nkFloat64Lit: result = result !& hash(cast[uint64](n.floatVal))
of nkStrLit..nkTripleStrLit: result = result !& hash(n.strVal)
of nkType, nkNilLit: result = result !& hash(n.typ.itemId)
of nkType, nkNilLit: result = result !& hash(n.typ.bindingId)
else:
for i in 0..<n.len:
result = result !& hashTree(n[i])

View File

@@ -172,9 +172,9 @@ proc backendTypeName(t: PType; conf: ConfigRef): string =
result = "`t"
result.addInt ord(t.kind)
result.add '.'
result.addInt t.uniqueId.item
result.addInt t.itemId.item
result.add '.'
result.add modname(t.uniqueId.module, conf)
result.add modname(t.itemId.module, conf)
result.add "@bk"
proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef) =
@@ -186,7 +186,7 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef
assert c.tl != nil
c.tl(t)
if t.uniqueId.isBackendMinted:
if t.itemId.isBackendMinted:
# Backend-minted (lower-stage) closure-env types key by their stable NIF name,
# never by structure (which diverges across the NIF boundary). An env `ref`
# that is itself NOT backend-minted still keys stably: it recurses here and
@@ -335,9 +335,9 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef
# mutation that an assertion deeper in `treeKey` left unrestored would
# corrupt the type. `symKey` above already emitted the type's identity,
# so on a back-reference we simply stop.
if not containsOrIncl(c.visited, t.itemId):
if not containsOrIncl(c.visited, t.bindingId):
c.treeKey(t.nImpl, flags + {CoHashTypeInsideNode}, conf)
c.visited.excl t.itemId
c.visited.excl t.bindingId
else:
c.m.addIdent "´empty"
# Object inheritance is part of identity: key the base class too.

View File

@@ -11,7 +11,7 @@
import
ast, astalgo, trees, msgs, platform, renderer, options,
lineinfos, int128, modulegraphs, astmsgs
lineinfos, int128, modulegraphs, astmsgs, bnode
import std/[intsets, strutils]
@@ -102,7 +102,7 @@ proc isPureObject*(typ: PType): bool =
proc isUnsigned*(t: PType): bool =
t.skipTypes(abstractInst).kind in {tyChar, tyUInt..tyUInt64}
proc getOrdValueAux*(n: PNode, err: var bool): Int128 =
proc getOrdValueAux*(n: AnyNode, err: var bool): Int128 =
var k = n.kind
if n.typ != nil and n.typ.skipTypes(abstractInst).kind in {tyChar, tyUInt..tyUInt64}:
k = nkUIntLit
@@ -119,17 +119,17 @@ proc getOrdValueAux*(n: PNode, err: var bool): Int128 =
of nkNilLit:
int128.Zero
of nkHiddenStdConv:
getOrdValueAux(n[1], err)
getOrdValueAux(n.secondSon, err)
else:
err = true
int128.Zero
proc getOrdValue*(n: PNode): Int128 =
proc getOrdValue*(n: AnyNode): Int128 =
var err: bool = false
result = getOrdValueAux(n, err)
#assert err == false
proc getOrdValue*(n: PNode, onError: Int128): Int128 =
proc getOrdValue*(n: AnyNode, onError: Int128): Int128 =
var err = false
result = getOrdValueAux(n, err)
if err:
@@ -1392,17 +1392,17 @@ proc classify*(t: PType): OrdinalType =
result = IntLike
else: result = NoneLike
proc skipConv*(n: PNode): PNode =
proc skipConv*[T: AnyNode](n: T): T =
result = n
case n.kind
of nkObjUpConv, nkObjDownConv, nkChckRange, nkChckRangeF, nkChckRange64:
# only skip the conversion if it doesn't lose too important information
# (see bug #1334)
if n[0].typ.classify == n.typ.classify:
result = n[0]
if n.firstSon.typ.classify == n.typ.classify:
result = n.firstSon
of nkHiddenStdConv, nkHiddenSubConv, nkConv:
if n[1].typ.classify == n.typ.classify:
result = n[1]
if n.secondSon.typ.classify == n.typ.classify:
result = n.secondSon
else: discard
proc skipHidden*(n: PNode): PNode =

View File

@@ -90,30 +90,30 @@ proc collectVTableDispatchers*(g: ModuleGraph) =
sortBucket(g.methods[bucket].methods, relevantCols)
let base = g.methods[bucket].methods[^1]
let baseType = base.typ.firstParamType.skipTypes(skipPtrs-{tyTypeDesc})
if baseType.itemId in g.objectTree and not containGenerics(baseType, g.objectTree[baseType.itemId]):
let methodIndexLen = g.bucketTable[baseType.itemId]
if baseType.itemId notin itemTable: # once is enough
if baseType.bindingId in g.objectTree and not containGenerics(baseType, g.objectTree[baseType.bindingId]):
let methodIndexLen = g.bucketTable[baseType.bindingId]
if baseType.bindingId notin itemTable: # once is enough
rootTypeSeq.add baseType
itemTable[baseType.itemId] = newSeq[PSym](methodIndexLen)
itemTable[baseType.bindingId] = newSeq[PSym](methodIndexLen)
sort(g.objectTree[baseType.itemId], cmp = proc (x, y: tuple[depth: int, value: PType]): int =
sort(g.objectTree[baseType.bindingId], cmp = proc (x, y: tuple[depth: int, value: PType]): int =
if x.depth >= y.depth: 1
else: -1
)
for item in g.objectTree[baseType.itemId]:
if item.value.itemId notin itemTable:
itemTable[item.value.itemId] = newSeq[PSym](methodIndexLen)
for item in g.objectTree[baseType.bindingId]:
if item.value.bindingId notin itemTable:
itemTable[item.value.bindingId] = newSeq[PSym](methodIndexLen)
var mIndex = 0 # here is the correpsonding index
if baseType.itemId notin rootItemIdCount:
rootItemIdCount[baseType.itemId] = 1
if baseType.bindingId notin rootItemIdCount:
rootItemIdCount[baseType.bindingId] = 1
else:
mIndex = rootItemIdCount[baseType.itemId]
rootItemIdCount.inc(baseType.itemId)
mIndex = rootItemIdCount[baseType.bindingId]
rootItemIdCount.inc(baseType.bindingId)
for idx in 0..<g.methods[bucket].methods.len:
let obj = g.methods[bucket].methods[idx].typ.firstParamType.skipTypes(skipPtrs)
itemTable[obj.itemId][mIndex] = g.methods[bucket].methods[idx]
itemTable[obj.bindingId][mIndex] = g.methods[bucket].methods[idx]
g.addDispatchers genVTableDispatcher(g, g.methods[bucket].methods, mIndex)
else: # if the base object doesn't have this method
g.addDispatchers genIfDispatcher(g, g.methods[bucket].methods, relevantCols, g.idgen)
@@ -128,40 +128,40 @@ proc sortVTableDispatchers*(g: ModuleGraph) =
sortBucket(g.methods[bucket].methods, relevantCols)
let base = g.methods[bucket].methods[^1]
let baseType = base.typ.firstParamType.skipTypes(skipPtrs-{tyTypeDesc})
if baseType.itemId in g.objectTree and not containGenerics(baseType, g.objectTree[baseType.itemId]):
let methodIndexLen = g.bucketTable[baseType.itemId]
if baseType.itemId notin itemTable: # once is enough
rootTypeSeq.add baseType.itemId
itemTable[baseType.itemId] = newSeq[PSym](methodIndexLen)
if baseType.bindingId in g.objectTree and not containGenerics(baseType, g.objectTree[baseType.bindingId]):
let methodIndexLen = g.bucketTable[baseType.bindingId]
if baseType.bindingId notin itemTable: # once is enough
rootTypeSeq.add baseType.bindingId
itemTable[baseType.bindingId] = newSeq[PSym](methodIndexLen)
sort(g.objectTree[baseType.itemId], cmp = proc (x, y: tuple[depth: int, value: PType]): int =
sort(g.objectTree[baseType.bindingId], cmp = proc (x, y: tuple[depth: int, value: PType]): int =
if x.depth >= y.depth: 1
else: -1
)
for item in g.objectTree[baseType.itemId]:
if item.value.itemId notin itemTable:
itemTable[item.value.itemId] = newSeq[PSym](methodIndexLen)
for item in g.objectTree[baseType.bindingId]:
if item.value.bindingId notin itemTable:
itemTable[item.value.bindingId] = newSeq[PSym](methodIndexLen)
var mIndex = 0 # here is the correpsonding index
if baseType.itemId notin rootItemIdCount:
rootItemIdCount[baseType.itemId] = 1
if baseType.bindingId notin rootItemIdCount:
rootItemIdCount[baseType.bindingId] = 1
else:
mIndex = rootItemIdCount[baseType.itemId]
rootItemIdCount.inc(baseType.itemId)
mIndex = rootItemIdCount[baseType.bindingId]
rootItemIdCount.inc(baseType.bindingId)
for idx in 0..<g.methods[bucket].methods.len:
let obj = g.methods[bucket].methods[idx].typ.firstParamType.skipTypes(skipPtrs)
if obj.itemId notin itemTable:
itemTable[obj.itemId] = newSeq[PSym](methodIndexLen)
itemTable[obj.itemId][mIndex] = g.methods[bucket].methods[idx]
if obj.bindingId notin itemTable:
itemTable[obj.bindingId] = newSeq[PSym](methodIndexLen)
itemTable[obj.bindingId][mIndex] = g.methods[bucket].methods[idx]
for baseType in rootTypeSeq:
g.setMethodsPerType(baseType, itemTable[baseType])
for item in g.objectTree[baseType]:
let typ = item.value.skipTypes(skipPtrs)
let idx = typ.itemId
let idx = typ.bindingId
for mIndex in 0..<itemTable[idx].len:
if itemTable[idx][mIndex] == nil:
let parentIndex = typ.baseClass.skipTypes(skipPtrs).itemId
let parentIndex = typ.baseClass.skipTypes(skipPtrs).bindingId
itemTable[idx][mIndex] = itemTable[parentIndex][mIndex]
g.setMethodsPerType(idx, itemTable[idx])

133
doc/ic.md
View File

@@ -2,12 +2,23 @@
Incremental Compilation (IC)
======================================
The ``nim ic`` command provides incremental compilation for Nim projects. It
decomposes compilation into per-module steps whose results are cached as NIF
files, and uses the external ``nifmake`` build tool to re-run only the steps
whose inputs changed.
``--ic:on`` turns an ordinary compile into an incremental one. It decomposes
compilation into per-module steps whose results are cached as NIF files, and
uses the external ``nifmake`` build tool to re-run only the steps whose inputs
changed.
This document describes **how `nim ic` works today**, including the edge cases
.. code-block:: cmd
nim c --ic:on myproject.nim
nim cpp --ic:on myproject.nim
It is a switch on the normal compile commands, not a command of its own, so
everything else keeps working unchanged: ``cpp`` and ``objc`` backends, ``-r``,
``-d:release``, ``--exceptions:``, and a project-wide opt-in from ``nim.cfg`` /
``config.nims``. The older spelling ``nim ic`` still works and drives the same
code, but it is the C backend only and cannot run the binary it built.
This document describes **how IC works today**, including the edge cases
that shaped the current design. The per-module backend rewrite that earlier
editions of this document listed as a *Plan* has **landed**: the whole-program,
reuse/redirect/def-retention backend is gone and codegen is now a set of
@@ -16,7 +27,7 @@ reuse/redirect/def-retention backend is gone and codegen is now a set of
Overview
========
The pipeline has two halves driven by one process (`nim ic`, `commandIc` in
The pipeline has two halves driven by one process (the *driver*, `commandIc` in
``compiler/deps.nim``) that constructs a dependency graph, writes a build file,
and hands it to ``nifmake``:
@@ -210,15 +221,17 @@ Edge cases (and why the machinery exists)
- **`nil` sons of loaded ASTs.** NIF dot-tokens load as `nil` where from-source
ASTs have `nkEmpty`; several passes gained `nil` guards.
- **Sealed loaded types.** Loaded types are `Sealed`; sem/transform mutate via
`unsealForTransform`/`exactReplica(idgen)` (the latter mints a fresh `uniqueId`
so serialized replicas don't collapse).
`unsealForTransform`/`copyType`, or -- where the copy must still answer to the
original in the generic binding tables -- `exactReplica(idgen)`, which gives the
copy its own `itemId` (so serialized replicas don't collapse) while inheriting
the original's `bindingId`.
- **Methods/RTTI ownership.** RTTI and type-bound hooks are emit-everywhere at
`cg` and deduplicated by the `merge` stage, like generic instances; the main
module's `cg` owns the whole-program method dispatchers.
- **Config cost.** Each child re-parsing `nim.cfg` + re-running `config.nims` in
the VM was ~80 ms; replaced by a precompiled `ic_config.cfg.nif` replayed in
`loadConfigs` (`compiler/icconfig.nim`).
- **`koch bootic`** bootstraps the compiler through `nim ic` (a 3-iteration
- **`koch bootic`** bootstraps the compiler through `--ic:on` (a 3-iteration
fixed-point check). It writes its binary to ``bin/nim_ic`` and never clobbers
``bin/nim``.
@@ -245,7 +258,7 @@ Known residual hack
Status and performance
======================
`nim ic` self-builds the compiler (`koch bootic`'s byte-identical fixed-point
IC self-builds the compiler (`koch bootic`'s byte-identical fixed-point
check) under both `orc` and `--mm:refc`, and passes the external-package CI set.
Cold full bootstrap on a 32-core box (`-d:release`, **no edits** — IC's worst
@@ -254,7 +267,7 @@ case, since incremental reuse is not exercised):
| | wall | notes |
| - | ---- | ----- |
| `koch boot` (classic) | ~1m00s | reference |
| `koch bootic` (`nim ic`) | ~1m39s | **~1.66×** |
| `koch bootic` (`--ic:on`) | ~1m39s | **~1.66×** |
This is down from ~7.5× in the whole-program-backend era. IC does modestly more
aggregate work (more processes, NIF re-parsing of imports per process), but on a
@@ -403,3 +416,101 @@ See also
- NIF format spec: [nifspec/doc/nif-spec.md](../nifspec/doc/nif-spec.md)
- NIFC (C-like target) spec: dist/nimony/doc/nifc-spec.md
Testing IC
==========
Two mechanisms, at very different scales.
**`tests/ic` — metamorphic tests.** A `t*.nim` whose body contains `#? metamorphic`
drives a sequence of cross-module edits through the IC driver in one fixed build
directory (see `testament/categories.nim`, `runMetamorphicIcTest`). Directives:
| directive | effect |
| --------- | ------ |
| ``#!FILE <name>`` | (re)write a module in the virtual file system |
| ``#!DELETE <name>`` | remove a module, from the vfs and from disk |
| ``#!FLAGS <switches>`` | change the compiler switches from here on |
| ``#!STEP <attrs>`` | materialise the files, build, run, check |
Step attributes: ``expect: <stdout>``, ``fails: <substring>`` (BOTH compilers must
reject it, with that text), ``noop``, ``body-edit``, ``iface-edit``,
``modules: <n>``, ``clean``, ``no-oracle``.
Every successful step is **also compiled with `nim c` and run, and the two
outputs must agree**. That oracle is the only check in the suite that is not
IC-against-IC: `clean == incremental`, `noop changes nothing` and the cookie
invariants are all satisfied by an IC that is *consistently* wrong, which is how
two silent miscompilations survived (a NIF-loaded module's `sfInjectDestructors`
was lost, so top-level destructors were never injected; `nfFirstWrite`/`nfLastRead`
had nowhere to live on a serialized sym node, so every first assignment to a
destructor-bearing local became `=sink` over zeroed memory). `koch bootic` has the
same blind spot — it proves the compiler reproduces *itself*.
**`testament --ic` — the whole corpus.** Appends `--ic:on` to every C and C++
test compile, so IC inherits the existing ~10k programs and their expected
output instead of the handful written for it by hand. Because it is a switch and
not a command, a test that overrides the command wholesale (`cmd: "nim cpp -r
$file"`) simply gains the switch — no verb rewriting, and the C++ corpus comes
along for free. Each also gets a private nimcache; without one they would share
a cache and thrash it.
To keep that affordable, testament borrows nimony's hastur model
(`warmupSharedCache` + `prefillFromWarmup`): a generated warmup program pulling in
`system` and the most-imported stdlib modules is compiled once per distinct
compile configuration into `nimcache/ic_warmup_<hash>`, and each test's empty
cache is seeded from it with **mtimes preserved** (nifmake compares
output-mtime > input-mtime, so stamping the copies "now" would re-fire the whole
graph). Only program-independent artifacts are copied — the frontend NIFs and
cookies plus the per-module `lower`/`cg` outputs. The `.c`/`.o` are deliberately
left behind: the merge decision (which module owns each emit-everywhere
definition) is whole-program, so those are re-rendered for every program anyway.
Measured on `tests/destructor` (97 test runs, 32-core box):
| | cold | warm |
| - | ---- | ---- |
| `nim c` | 35s | 32s |
| `--ic:on` | ~3m30 | **9.8s** |
The warm number is the developer loop and it is 3.2x faster than the classic
backend; the cold number is paid once per configuration and then cached on disk.
The disk cost is real and worth knowing: ~3.4 GB of nimcache for that one
category.
One property of an incremental compiler is worth spelling out because it looks
like a test bug: **a cached stage emits no diagnostics**. `--expandArc` output, a
hint, a warning — all of it is produced by the process that actually runs, so a
build that reuses every artifact prints nothing. Tests that check `nimout` (and
anything you are debugging by eye) therefore need a cold cache; running the same
test twice in a row makes the second run's `nimout` empty.
The C++ backend
===============
``nim cpp --ic:on`` works, and `tests/cpp` passes under it. Three things had to
change for that, and they are worth knowing because they are the shape of every
"C++ needs the whole program" problem the per-module backend has:
* **The driver must name the right file.** ``deps.nim`` DECLARES each module's
translation unit to ``nifmake`` without loading a single module, so it cannot
ask ``cgen.getCFile``; ``options.icCFileExt`` mirrors that formula at backend
granularity (``.nim.cpp`` / ``.nim.m`` / ``.nim.c``).
* **C++ has no designated initializers**, so the RTTI record is a bare variable
that ``DatInit`` fills field by field. That bare ``TNimTypeV2 x;`` is a
tentative definition, which C's linker merges and C++'s does not — every TU
that demanded the type defined it. It now gets the same extern-declaration +
owned-``'d'``-definition split the C flavour has.
* **A C++ member is declared inside its class.** ``memberProcsPerType`` and
``initializersPerType`` live only in the sem process, so the backend emitted
the struct WITHOUT its member declarations; they are replayed from a
``(repcppmember …)`` log entry now (``modulegraphs.replayCppMember`` re-derives
the type from the routine's signature, exactly as ``semCppMember`` does).
Two follow-on details: a member's ``loc.snippet`` is a CALL PATTERN
(``#->salute(@)``), so it must be computed even in the TU that only *calls* the
member (whole-program cgen got that for free by generating the defining module
first), and it is not a linker name — every ``salute`` member in every class
mints the same one, so definitions are keyed by their NIF name in the merge
stage instead.

View File

@@ -16,11 +16,12 @@ const
ChecksumsStableCommit = "5c132cd332cce5d64a0da9ac3e4c9664313dccb4" # 0.2.2
SatStableCommit = "9d52513b3c68bfb929dbd687d4fb2836cfee6936"
NimonyStableCommit = "f831b953d7c21d9a4b11d0042039e7f84d7c8dc9" # unversioned \
NimonyStableCommit = "1721aab3cad18663da92c2b85508b1f2ff73e3df" # unversioned \
# Note that Nimony uses Nim as a git submodule but we don't want to install
# Nimony's dependency to Nim as we are Nim. So a `git clone` without --recursive
# is **required** here.
# Commit from 2026-07-10 -- stable .bif file format
# Commit from 2026-08-31 -- nifcore-based lib; `bif.load` fills pools with
# `addOrdered` instead of hashing every entry it just read back in order.
# examples of possible values for fusion: #head, #ea82b54, 1.2.3
FusionStableHash = "#562467452b32cb7a97410ea177f083e6d8405734"
@@ -76,7 +77,7 @@ Options:
--skipIntegrityCheck skips integrity check when booting the compiler
Possible Commands:
boot [options] bootstraps with given command line options
bootic [options] bootstraps via the incremental compiler (`nim ic`)
bootic [options] bootstraps via the incremental compiler (`--ic:on`)
distrohelper [bindir] helper for distro packagers
tools builds Nim related tools
toolsNoExternal builds Nim related tools (except external tools,
@@ -196,10 +197,31 @@ proc bundleChecksums(latest: bool) =
# to `koch boot`, but `nimCompileFold` spawns a fresh `nim c` that would
# otherwise inherit the ambient configuration.
const nifOptions = "-d:release --noNimblePath --skipUserCfg --skipParentCfg"
if not fileExists("bin/nifler".exe):
nimCompileFold("Compile nifler", "dist/nimony/src/nifler/nifler.nim", options = nifOptions)
if not fileExists("bin/nifmake".exe):
nimCompileFold("Compile nifmake", "dist/nimony/src/nifmake/nifmake.nim", options = nifOptions)
# Rebuilding these only when the binary is ABSENT silently keeps the tools of
# the PREVIOUS pin: bump `NimonyStableCommit` in a checkout that already has
# `bin/nifler`, and the compiler links the new `dist/nimony/src/lib` while
# `nifler`/`nifmake` still speak the old one. A fresh CI checkout has no
# `bin/`, so it builds them and looks green — only the working tree that
# already has them breaks, which is the worst way round to find out. So stamp
# each tool with the nimony commit it came from and rebuild on a mismatch.
# If the commit cannot be determined (a bundled `dist` with no `.git`), fall
# back to the old build-if-absent rule rather than rebuilding every time.
let nimonyHead = block:
let (outp, status) = osproc.execCmdEx(
"git -C " & quoteShell(distDir / "nimony") & " rev-parse HEAD")
if status == 0: outp.strip else: ""
proc bundleNifTool(name, src: string) =
let stamp = "bin" / ("." & name & ".nimony-commit")
let builtFrom = if fileExists(stamp): readFile(stamp).strip else: ""
if not fileExists(("bin" / name).exe) or
(nimonyHead.len > 0 and builtFrom != nimonyHead):
nimCompileFold("Compile " & name, src, options = nifOptions)
if nimonyHead.len > 0: writeFile(stamp, nimonyHead)
bundleNifTool("nifler", "dist/nimony/src/nifler/nifler.nim")
bundleNifTool("nifmake", "dist/nimony/src/nifmake/nifmake.nim")
proc bundleNimsuggest(args: string) =
bundleChecksums(false)
@@ -450,7 +472,7 @@ proc bootic(args: string, skipIntegrityCheck: bool) =
# everything.
if i > 0: removeDir smartNimcache
let nimi = if i == 0: nimStart else: i.thVersion
exec "$# ic --nimcache:$# $# compiler" / "nim.nim" %
exec "$# c --ic:on --nimcache:$# $# compiler" / "nim.nim" %
[nimi, smartNimcache, args]
if sameFileContent(output, i.thVersion):
copyExe(output, finalDest)
@@ -615,7 +637,7 @@ proc runIcTestFile(inp: string) =
for fragment in content.split("#!EDIT!#"):
let file = inp.replace(".nim", "_temp.nim")
writeFile(file, fragment)
var cmd = nimExe & " ic --hint:Conf:off --warnings:off "
var cmd = nimExe & " c --ic:on --hint:Conf:off --warnings:off "
cmd.add quoteShell(file)
exec(cmd)
@@ -625,7 +647,7 @@ proc runIcTestFile(inp: string) =
const icSuite = ["thallo", "tconverter", "timp", "tmiscs", "tparseutils",
"tcompiletimeglobal", "tsighashstable", "tpureenum", "tgenericoffer",
"tconverterreexport", "ttypeoffer", "ttransitiveoffer",
"tmodsymref", "tmethupref", "temit", "ttraitparam"]
"tmodsymref", "tmethupref", "temit", "ttraitparam", "tnestasgn"]
proc icTest(args: string) =
temp("")

View File

@@ -17,6 +17,7 @@ __AVR__
__arm__
__riscv
__EMSCRIPTEN__
__unix__
*/
@@ -597,7 +598,7 @@ NIM_STATIC_ASSERT(sizeof(NI) == sizeof(void*) && NIM_INTBITS == sizeof(NI)*8, "P
#define nimMulInt64(a, b, res) __builtin_smulll_overflow(a, b, (long long int*)res)
#if NIM_INTBITS == 32
#if (defined(__arm__) || defined(__riscv)) && defined(__GNUC__)
#if ((defined(__arm__) && !defined(__unix__)) || defined(__riscv)) && defined(__GNUC__)
/* arm-none-eabi-gcc and riscv32-unknown-elf-gcc targets define int32_t as long int */
#define nimAddInt(a, b, res) __builtin_saddl_overflow(a, b, res)
#define nimSubInt(a, b, res) __builtin_ssubl_overflow(a, b, res)

View File

@@ -3304,13 +3304,21 @@ proc dirInclude(p: var RstParser): PRstNode =
## Only the content before the first occurrence of the specified
## text (but after any after text) will be included. If text is
## not found inclusion will happen until the end of the file.
#literal : flag (empty)
# The entire included text is inserted into the document as a single
# literal block (useful for program listings).
#encoding : name of text encoding
# The text encoding of the external data file. Defaults to the document's
# encoding (if specified).
#
##
## :literal: flag (empty)
##
## The entire included text is inserted into the document as a single
## literal block (useful for program listings).
##
## :code: language (if empty, `nim` is assumed by default)
##
## The argument and the included content are passed to the code directive
## (useful for program listings).
##
## :encoding: name of text encoding
##
## The text encoding of the external data file. Defaults to the document's
## encoding (if specified).
result = nil
var n = parseDirective(p, rnDirective, {hasArg, argIsFile, hasOptions}, nil)
var filename = strip(addNodes(n.sons[0]))
@@ -3319,31 +3327,44 @@ proc dirInclude(p: var RstParser): PRstNode =
rstMessage(p, meCannotOpenFile, filename)
else:
# XXX: error handling; recursive file inclusion!
let inputString = readFile(path)
let startPosition =
block:
let searchFor = n.getFieldValue("start-after").strip()
if searchFor != "":
let pos = inputString.find(searchFor)
if pos != -1: pos + searchFor.len
else: 0
else:
0
let endPosition =
block:
let searchFor = n.getFieldValue("end-before").strip()
if searchFor != "":
let pos = inputString.find(searchFor, start = startPosition)
if pos != -1: pos - 1
else: 0
else:
inputString.len - 1
if getFieldValue(n, "literal") != "":
result = newRstNode(rnLiteralBlock)
result.add newLeaf(readFile(path))
result.add newLeaf(inputString[startPosition..endPosition])
elif getFieldValue(n, "code") != "":
result = newRstNode(rnCodeBlock)
result.sons.setLen(3)
let lang = getFieldValue(n, "code").strip()
if lang notin ["", "\x01\x01"]:
var codeArg = newRstNode(rnDirArg)
codeArg.add(newLeaf(lang))
result.sons[0] = codeArg
result.sons[1] = newRstNode(rnFieldList)
defaultCodeLangNim(p, result)
var litBlock = newRstNode(rnLiteralBlock)
litBlock.add newLeaf(inputString[startPosition..endPosition])
result.sons[2] = litBlock
else:
let inputString = readFile(path)
let startPosition =
block:
let searchFor = n.getFieldValue("start-after").strip()
if searchFor != "":
let pos = inputString.find(searchFor)
if pos != -1: pos + searchFor.len
else: 0
else:
0
let endPosition =
block:
let searchFor = n.getFieldValue("end-before").strip()
if searchFor != "":
let pos = inputString.find(searchFor, start = startPosition)
if pos != -1: pos - 1
else: 0
else:
inputString.len - 1
var q: RstParser
initParser(q, p.s)
let saveFileIdx = p.s.currFileIdx

View File

@@ -152,9 +152,11 @@ proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] =
raise newException(ValueError, "Invalid request protocol. Got: " &
protocol)
result.orig = protocol
i.inc protocol.parseSaturatedNatural(result.major, i)
if i < protocol.len: inc i # Skip .
i.inc protocol.parseSaturatedNatural(result.minor, i)
var n = protocol.parseSaturatedNatural(result.major, i)
i.inc n
if i < protocol.len and protocol[i] == '.':
inc i
n = protocol.parseSaturatedNatural(result.minor, i)
proc sendStatus(client: AsyncSocket, status: string): Future[void] =
client.send("HTTP/1.1 " & status & "\c\L\c\L")

View File

@@ -238,6 +238,7 @@ proc fromJson*[T](a: var T, b: JsonNode, opt = Joptions()) =
a = T()
fromJson(a[], b, opt)
elif T is array:
checkJson b.kind == JArray
checkJson a.len == b.len, "Json array size doesn't match for " & $T
var i = 0
for ai in mitems(a):
@@ -248,6 +249,7 @@ proc fromJson*[T](a: var T, b: JsonNode, opt = Joptions()) =
for val in b.getElems:
incl a, jsonTo(val, E)
elif T is seq:
checkJson b.kind == JArray
a.setLen b.len
for i, val in b.getElems:
fromJson(a[i], val, opt)

View File

@@ -1254,7 +1254,9 @@ proc del*[T](x: var seq[T], i: Natural) {.noSideEffect.} =
a.del(2)
assert a == @[10, 11, 14, 13]
let xl = x.len - 1
movingCopy(x[i], x[xl])
# Avoid moving the element onto itself when deleting the last item.
if i != xl:
movingCopy(x[i], x[xl])
setLen(x, xl)
proc insert*[T](x: var seq[T], item: sink T, i = 0.Natural) {.noSideEffect.} =

View File

@@ -155,15 +155,17 @@ type
MemRegion = object
when usesRegionHandles:
# Keeping the handle here does change the layout, but until proven otherwise
# this layout is more readable and shouldn't regress performance.
regionHandle: ptr RegionHandle
when not defined(gcDestructors):
minLargeObj, maxLargeObj: int
freeSmallChunks: array[0..max(1, SmallChunkSize div MemAlign-1), PSmallChunk]
# List of available chunks per size class. Only one is expected to be active per class.
when defined(gcDestructors):
when defined(gcDestructors) and not usesRegionHandles:
sharedFreeLists: SharedFreeLists
# Used directly without threads. Threaded builds use RegionHandle but
# retain this 2 KiB spacer: removing it regresses 2-4 KiB allocations.
# Remote-free buckets live on the MemRegion when there is no
# RegionHandle. Threaded memory managers with handles keep them on the handle instead.
flBitmap: uint32
slBitmap: array[RealFli, uint32]
matrix: array[RealFli, array[MaxSli, PBigChunk]]
@@ -963,13 +965,19 @@ proc bigChunkAlignOffset(alignment: int): int {.inline.} =
else:
result = align(sizeof(BigChunk) + sizeof(FreeCell), alignment) - sizeof(BigChunk) - sizeof(FreeCell)
proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer =
template rawAllocAux(aligned: static bool) {.dirty.} =
when defined(nimTypeNames):
inc(a.allocCounter)
sysAssert(allocInv(a), "rawAlloc: begin")
sysAssert(roundup(65, 8) == 72, "rawAlloc: roundup broken")
var size = roundup(requestedSize, max(MemAlign, alignment))
let alignOff = smallChunkAlignOffset(alignment)
when aligned:
var size = roundup(requestedSize, max(MemAlign, alignment))
let alignOff = smallChunkAlignOffset(alignment)
else:
# Common `alloc` path: no custom alignment. Keep this a separate
# instantiation so clang does not emit `smallChunkAlignOffset(0)`.
var size = (requestedSize + (MemAlign - 1)) and not (MemAlign - 1)
const alignOff = 0
sysAssert(size >= sizeof(FreeCell), "rawAlloc: requested size too small")
sysAssert(size >= requestedSize, "insufficient allocated size!")
#c_fprintf(stdout, "alloc; size: %ld; %ld\n", requestedSize, size)
@@ -986,11 +994,13 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer
if atomicLoadN(sharedHead, ATOMIC_RELAXED) != nil:
tc.freeList = atomicExchangeN(sharedHead, nil, ATOMIC_ACQUIRE)
else:
tc.freeList = a.sharedFreeLists[s]
a.sharedFreeLists[s] = nil
# If `tc.freeList` isn't nil, `tc` gains capacity. Calculate how
# much it gained and how many foreign cells are included.
compensateCounters(a, tc, size)
let sharedHead = addr a.sharedFreeLists[s]
tc.freeList = sharedHead[]
sharedHead[] = nil
# Empty peeks are the common local case; skip the walk and the
# `free += 0` / `occ -= 0` stores clang would otherwise keep.
if tc.freeList != nil:
compensateCounters(a, tc, size)
# allocate a small block: for small chunks, we use only its next pointer
let s = size div MemAlign
@@ -1071,7 +1081,7 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer
# For big chunks with custom alignment, allocate extra space.
# Since chunks are page-aligned, the needed padding is a compile-time
# deterministic value rather than a worst-case estimate.
let alignPad = bigChunkAlignOffset(alignment)
let alignPad = when aligned: bigChunkAlignOffset(alignment) else: 0
size = requestedSize + bigChunkOverhead() + alignPad
# allocate a large block
var c = if size >= HugeChunkSize: getHugeChunk(a, size)
@@ -1096,6 +1106,12 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer
when defined(heaptrack):
heaptrack_malloc(result, requestedSize)
proc rawAlloc(a: var MemRegion, requestedSize: int): pointer =
rawAllocAux(false)
proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int): pointer =
rawAllocAux(true)
proc rawAlloc0(a: var MemRegion, requestedSize: int): pointer =
result = rawAlloc(a, requestedSize)
zeroMem(result, requestedSize)

View File

@@ -516,7 +516,15 @@ proc testNimblePackages(r: var TResults; cat: Category; packageFilter: string) =
# accumulated file set is materialised before each `#!STEP`. A `#!STEP`'s
# attributes are `;`-separated, each either `key: value` or a bare flag:
# expect: <stdout> noop body-edit iface-edit modules: <n> clean
# fails: <substring> no-oracle
# The last step always also runs the clean==incremental check.
#
# Every successful step is ALSO compiled with `nim c` and run, and the two
# outputs must agree (`no-oracle` opts out). This is the only check in the suite
# that is not IC-against-IC; without it a consistently wrong IC passes
# everything. `#!DELETE <file>` removes a module, `#!FLAGS <switches>` changes
# the compiler switches from that point on, and `fails: <text>` asserts that
# BOTH compilers reject the program with that text.
type MetamorphicError = object of CatchableError
resultKind: TResultEnum
@@ -590,16 +598,34 @@ proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options:
let buildDir = (file.changeFileExt("") & "_mm").absolutePath
let nc = buildDir / "nc"
let bin = buildDir / "prog".addFileExt(ExeExt)
# The ORACLE: the same sources compiled by the classic backend. Every
# invariant this runner checked before was IC-against-IC (clean == incremental,
# no-op changes nothing, ...), which a *consistently* wrong IC satisfies
# perfectly — that is how a whole class of silent miscompilations (top-level
# destructors never injected; `nfFirstWrite`/`nfLastRead` dropped by the
# serializer, so every first assignment to a destructor-bearing local became
# `=sink` over zeroed memory) stayed invisible. `nim c` is the reference the
# suite was missing.
let ncRef = buildDir / "ncref"
let binRef = buildDir / "progref".addFileExt(ExeExt)
removeDir(buildDir)
createDir(buildDir)
# Extra switches for both compilers, settable per step via `#!FLAGS`.
var extraFlags: seq[string] = @[]
template compileIc(): untyped =
execCmdEx2(compilerPrefix, ["ic", "--hint:Conf:off", "--warnings:off",
"--nimcache:" & nc, "--out:" & bin, "main.nim"],
execCmdEx2(compilerPrefix, @["ic", "--hint:Conf:off", "--warnings:off",
"--nimcache:" & nc, "--out:" & bin] & extraFlags & @["main.nim"],
workingDir = buildDir)
template compileRef(): untyped =
execCmdEx2(compilerPrefix, @["c", "--hint:Conf:off", "--warnings:off",
"--nimcache:" & ncRef, "--out:" & binRef] & extraFlags & @["main.nim"],
workingDir = buildDir)
# Parse the source into a flat op list: ("file", name, content) | ("step", attrs, "").
type OpKind = enum opFile, opStep
type OpKind = enum opFile, opStep, opDelete, opFlags
type Op = object
kind: OpKind
a, b: string
@@ -615,6 +641,18 @@ proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options:
if s.startsWith("#!FILE"):
flushFile()
curName = s["#!FILE".len .. ^1].strip
elif s.startsWith("#!DELETE"):
# Remove a module from the virtual file system AND from disk. Deleting a
# still-imported file moves no mtime, so nothing in an mtime-keyed build
# re-fires: `nim ic` used to relink a stale binary where `nim c` reports
# `cannot open file`. Untestable until the format could express it.
flushFile()
ops.add Op(kind: opDelete, a: s["#!DELETE".len .. ^1].strip)
elif s.startsWith("#!FLAGS"):
# Change the compiler switches for the following steps. Config changes
# are not files, so an mtime-keyed build cannot see them either.
flushFile()
ops.add Op(kind: opFlags, a: s["#!FLAGS".len .. ^1].strip)
elif s.startsWith("#!STEP"):
flushFile()
ops.add Op(kind: opStep, a: s["#!STEP".len .. ^1].strip)
@@ -630,11 +668,21 @@ proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options:
var prevSnap = initTable[string, string]()
var prevBin = ""
var stepIdx = 0
var deleted: seq[string] = @[]
try:
for o in ops:
if o.kind == opFile:
case o.kind
of opFile:
vfs[o.a] = o.b
continue
of opDelete:
vfs.del o.a
deleted.add o.a
continue
of opFlags:
extraFlags = o.a.splitWhitespace()
continue
of opStep: discard
inc stepIdx
let where = "step " & $stepIdx
# Parse step attributes.
@@ -646,8 +694,36 @@ proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options:
if c >= 0: attrs[p[0 ..< c].strip] = p[c+1 .. ^1].strip
else: attrs[p] = ""
for fn in deleted:
removeFile(buildDir / fn)
deleted.setLen 0
for fn, content in vfs: writeFile(buildDir / fn, content)
let (_, cout, ccode) = compileIc()
# `fails: <substring>` — the build MUST fail, with that text in its output.
# Without this every step had to succeed, so the whole error path was
# untested: a `nim m` that errored still wrote its `.s.bif`, nifmake then
# saw the rule satisfied, and the NEXT run reported success for a program
# that does not compile.
if "fails" in attrs:
if ccode == 0:
mmRaise(reBuildFailed, "a failed build", where & ": `nim ic` unexpectedly succeeded")
let want = attrs["fails"]
if want.len > 0 and want notin cout:
mmRaise(reOutputsDiffer, want, where & ": error text did not contain it:\n" & cout)
# The oracle must reject it too, else the test is asserting an IC-only
# error rather than a real one.
let (_, refOut, refCode) = compileRef()
if refCode == 0:
mmRaise(reBuildFailed, "`nim c` to fail too",
where & ": `nim ic` failed but `nim c` accepted the program:\n" & cout)
if want.len > 0 and want notin refOut:
mmRaise(reOutputsDiffer, want,
where & ": `nim c` failed differently:\n" & refOut)
prevSnap = snapshotDir(nc)
prevBin = ""
continue
if ccode != 0:
mmRaise(reBuildFailed, "", where & ": `nim ic` failed:\n" & cout)
let (_, rout, rcode) = execCmdEx2(bin.absolutePath, [], workingDir = buildDir)
@@ -658,6 +734,22 @@ proc runMetamorphicIcTest(r: var TResults; file: string; cat: Category; options:
if rout.strip == want.strip: discard
else: mmRaise(reOutputsDiffer, want, where & " output:\n" & rout.strip)
# ORACLE: same sources through the classic backend, same observable
# behaviour. Unlike `expect:` this needs no foresight from the test author —
# it compares everything the program does, not only what someone thought to
# print, which is exactly what a silently-skipped destructor evades.
block oracle:
if "no-oracle" in attrs: break oracle
let (_, refCout, refCcode) = compileRef()
if refCcode != 0:
mmRaise(reBuildFailed, "", where & ": `nim c` (oracle) failed:\n" & refCout)
let (_, refRout, refRcode) = execCmdEx2(binRef.absolutePath, [],
workingDir = buildDir)
if refRout.strip != rout.strip or refRcode != rcode:
mmRaise(reOutputsDiffer, "`nim c` output:\n" & refRout.strip,
where & ": `nim ic` disagrees with `nim c`\n ic (exit " & $rcode &
"):\n" & rout.strip & "\n c (exit " & $refRcode & "):\n" & refRout.strip)
let snap = snapshotDir(nc)
let binBytes = stableBinary(bin)
if stepIdx > 1:
@@ -755,6 +847,12 @@ proc processSingleTest(r: var TResults, cat: Category, options, test: string, ta
let target = if cat.string.normalize == "js": targetJS else: targetC
targets = {target}
doAssert fileExists(test), test & " test does not exist"
# `testament r <file>` must dispatch metamorphic IC tests the same way
# `testament cat ic` does, otherwise a single-test run tries to parse the
# header as an ordinary spec and rejects it.
if isMetamorphicIcTest(readFile(test)):
runMetamorphicIcTest(r, test, cat, options)
return
testSpec r, makeTest(test, options, cat), targets
proc isJoinableSpec(spec: TSpec): bool =

View File

@@ -12,7 +12,7 @@
import
std/[strutils, pegs, os, osproc, streams, json,
parseopt, browsers, terminal, exitprocs,
algorithm, times, intsets, macros]
algorithm, times, intsets, macros, tables]
import backend, specs, azure, htmlgen
@@ -35,6 +35,12 @@ var simulate = false
var optVerbose = false
var useMegatest = true
var valgrindEnabled = true
var useIc = false
## `--ic`: compile every C-target test with `nim ic` instead of `nim c`, so the
## incremental compiler inherits the whole existing corpus (~10k programs with
## expected output) instead of the handful of tests written for it by hand.
## Every invariant the `tests/ic` suite checks is IC-against-IC; this is the
## part that compares IC against the reference backend at scale.
proc verboseCmd(cmd: string) =
if optVerbose:
@@ -58,6 +64,7 @@ Arguments:
Options:
--print print results to the console
--verbose print commands (compiling and running tests)
--ic compile C-target tests with `nim ic` (incremental)
--simulate see what tests would be run but don't run them (for debugging)
--failing only show failing/ignored tests
--targets:"c cpp js objc" run tests for specified targets (default: c)
@@ -155,11 +162,40 @@ proc execCmdEx2(command: string, args: openArray[string]; workingDir: string = "
if result.exitCode != -1: break
close(p)
proc nimcacheDir(filename, options: string, target: TTarget): string =
proc nimcacheDir(filename, options: string, target: TTarget,
extraOptions = ""): string =
## Give each test a private nimcache dir so they don't clobber each other's.
let hashInput = options & $target
## `extraOptions` (a `matrix:` entry) is part of the key: two matrix variants
## of one file are two different compilations, and sharing a cache between them
## means each run invalidates what the previous left. Harmless for the classic
## backend, which caches only object files, but it makes an incremental cache
## useless — every variant re-sems the world every time.
let hashInput = options & extraOptions & $target
result = "nimcache" / (filename & '_' & hashInput.getMD5)
const icWarmupSource = """
# Generated by testament for `--ic`. Compiling this once fills a shared IC cache
# with `system` and the stdlib modules the test corpus imports most, so each
# test's own cold build starts from precompiled NIFs instead of re-semming the
# world. Mirrors nimony's hastur `tools/warmup.nim` + `prefillFromWarmup`.
import std/[assertions, macros, strutils, tables, os, typetraits, sequtils,
sugar, math, options, times, json, sets, algorithm, hashes,
strformat, parseutils, streams, unicode]
proc icWarmupAnchor*(): int =
# Reference a few generic instantiations the corpus leans on so their
# `.c.nif` artifacts are precompiled too, not just the modules' interfaces.
var t = initTable[string, int]()
t["a"] = 1
var s = @[1, 2, 3]
s.sort()
result = s.len + t.len + "x".repeat(2).len
"""
var icWarmupCaches: Table[string, string]
## Compile-config key -> shared warm IC cache (or "" when unavailable).
var buildingIcWarmup = false
proc prepareTestCmd(cmdTemplate, filename, options, nimcache: string,
target: TTarget, extraOptions = ""): string =
var options = target.defaultOptions & ' ' & options
@@ -169,9 +205,103 @@ proc prepareTestCmd(cmdTemplate, filename, options, nimcache: string,
result = cmdTemplate % ["target", targetToCmd[target],
"options", options, "file", filename.quoteShell,
"filedir", filename.getFileDir(), "nim", compilerPrefix]
if useIc and target in {targetC, targetCpp}:
# `--ic:on` turns the ordinary compile command into the IC driver, so the
# verb is left alone: roughly half the corpus overrides the command wholesale
# (`cmd: "nim c --gc:arc $file"`), which neither goes through `$target` nor
# picks up `$options`, and such a test now simply gains the switch. Each also
# gets a private nimcache, which is what makes it incremental at all.
#
# Switches must land BEFORE the project file: anything after it is swallowed
# into `config.arguments`, and a non-empty `arguments` without `--run` is a
# hard error ("arguments can only be given if the '--run' option is
# selected").
var switches = "--ic:on "
if nimcache.len > 0 and "--nimCache:" notin result and "--nimcache:" notin result:
switches.add "--nimCache:" & nimcache.quoteShell & " "
# `rfind`, not `find`: the private nimcache path embeds the test's file name
# (`nimcache/tests/destructor/tmove.nim_<hash>`), so the FIRST occurrence is
# inside a switch's value. The project file is the last one.
let fileArg = filename.quoteShell
let at = result.rfind(fileArg)
if at >= 0: result = result[0 ..< at] & switches & result[at .. ^1]
else: result.add " " & switches
proc icWarmupCache(cmdTemplate, filename, options: string, target: TTarget,
extraOptions: string): string =
## The shared warm cache for this test's exact compile configuration, built on
## first use and kept in `nimcache/` across runs. Keyed by the switches AND the
## test's directory, because both decide what the artifacts contain: the
## switches through `-d:`/`--mm:` etc., the directory through the `nim.cfg` /
## `config.nims` it inherits. A cache built under a different configuration
## would just be invalidated wholesale on first use, which is worse than none.
if buildingIcWarmup: return ""
let dir = filename.getFileDir()
let key = options & extraOptions & $target & dir
if icWarmupCaches.hasKey(key): return icWarmupCaches[key]
result = "nimcache" / ("ic_warmup_" & key.getMD5)
icWarmupCaches[key] = result
if dirExists(result / "ic.version"): return # already built by an earlier run
if fileExists(result / "ic.version"): return
# The warmup must live in the test's own directory so it inherits the same
# config files; a stray `.nim` there is not picked up as a test (testament
# only collects `t*.nim`). The name must be a valid Nim identifier.
let src = dir / "icwarmup_generated.nim"
try:
writeFile(src, icWarmupSource)
except IOError, OSError:
icWarmupCaches[key] = ""
return ""
buildingIcWarmup = true
let cmd = prepareTestCmd(cmdTemplate, src, options, result, target, extraOptions)
let (outp, code) = execCmdEx(cmd)
buildingIcWarmup = false
try: removeFile(src)
except OSError: discard
if code != 0:
# Non-fatal: without a warm cache every test just pays its own cold build.
if optVerbose: echo "ic warmup failed: ", cmd, "\n", outp
icWarmupCaches[key] = ""
return ""
proc prefillIcCache(warmup, nimcache: string) =
## Seed a test's empty cache from the shared warm one. Only the artifacts that
## do NOT depend on which program is being built are copied: the frontend NIFs
## and cookies, plus the per-module `lower`/`cg` outputs. The `.c`/`.o` are
## deliberately left out — the merge decision (who owns each emit-everywhere
## definition) is whole-program, so those get re-rendered for every program
## anyway and copying them is pure I/O.
##
## Mtimes are preserved, and that is load-bearing: nifmake decides staleness by
## output-mtime > input-mtime, so stamping every prefilled file with "now"
## would scramble the DAG ordering the warmup established and re-fire the
## whole graph — exactly what the copy is meant to avoid.
if warmup.len == 0 or not dirExists(warmup): return
if dirExists(nimcache): return # the test already has its own cache
const wanted = [".p.nif", ".p.deps.nif", ".deps.nif", ".s.bif", ".iface.bif",
".impl.bif", ".edges.bif", ".s.deps.bif", ".t.bif",
".c.nif", ".cpp.nif"]
try:
createDir(nimcache)
for path in walkFiles(warmup / "*"):
let name = path.extractFilename
var take = name == "ic.version" or name == "ic_build_args.txt"
if not take:
for ext in wanted:
if name.endsWith(ext): take = true; break
if not take: continue
let dst = nimcache / name
copyFile(path, dst)
try: setLastModificationTime(dst, getLastModificationTime(path))
except OSError, IOError: discard
except OSError, IOError:
discard # best effort; a cold build still works
proc callNimCompiler(cmdTemplate, filename, options, nimcache: string,
target: TTarget, extraOptions = ""): TSpec =
if useIc and target in {targetC, targetCpp} and nimcache.len > 0 and not buildingIcWarmup:
prefillIcCache(icWarmupCache(cmdTemplate, filename, options, target, extraOptions),
nimcache)
result = TSpec(cmd: prepareTestCmd(cmdTemplate, filename, options, nimcache, target,
extraOptions))
verboseCmd(result.cmd)
@@ -415,21 +545,28 @@ proc cmpMsgs(r: var TResults, expected, given: TSpec, test: TTest,
result = r.finishTestRetryable(test, target, extraOptions, expected.msg, given.msg, reSuccess)
inc(r.passed)
proc generatedFile(test: TTest, target: TTarget): string =
proc generatedFile(test: TTest, target: TTarget, extraOptions: string): string =
if target == targetJS:
result = test.name.changeFileExt("js")
else:
let (_, name, _) = test.name.splitFile
let ext = targetToExt[target]
result = nimcacheDir(test.name, test.options, target) / "@m" & name.changeFileExt(ext)
# `extraOptions` must match what `testSpecWithNimcache` passed to the
# compiler — the matrix entry is part of the nimcache key, so leaving it out
# here looks for the `.c` of a DIFFERENT variant's cache (which does not
# exist) and every `ccodeCheck` test with a `matrix:` failed as
# `reCodeNotFound`.
result = nimcacheDir(test.name, test.options, target, extraOptions) /
"@m" & name.changeFileExt(ext)
proc needsCodegenCheck(spec: TSpec): bool =
result = spec.maxCodeSize > 0 or spec.ccodeCheck.len > 0
proc codegenCheck(test: TTest, target: TTarget, spec: TSpec, expectedMsg: var string,
proc codegenCheck(test: TTest, target: TTarget, extraOptions: string,
spec: TSpec, expectedMsg: var string,
given: var TSpec) =
try:
let genFile = generatedFile(test, target)
let genFile = generatedFile(test, target, extraOptions)
let contents = readFile(genFile)
for check in spec.ccodeCheck:
if check.len > 0 and check[0] == '\\':
@@ -457,7 +594,7 @@ proc compilerOutputTests(test: TTest, target: TTarget, extraOptions: string,
var givenmsg: string = ""
if given.err == reSuccess:
if expected.needsCodegenCheck:
codegenCheck(test, target, expected, expectedmsg, given)
codegenCheck(test, target, extraOptions, expected, expectedmsg, given)
givenmsg = given.msg
if not nimoutCheck(expected, given) or
not checkForInlineErrors(expected, given):
@@ -590,7 +727,7 @@ proc targetHelper(r: var TResults, test: TTest, expected: TSpec, extraOptions: s
inc count
echo "testSpec count: ", count, " expected: ", expected
else:
let nimcache = nimcacheDir(test.name, test.options, target)
let nimcache = nimcacheDir(test.name, test.options, target, extraOptions)
var testClone = test
let target = changeTarget(extraOptions, target)
testSpecHelper(r, testClone, expected, target, extraOptions, nimcache)
@@ -691,6 +828,7 @@ proc main() =
case p.key.normalize
of "print": optPrintResults = true
of "verbose": optVerbose = true
of "ic": useIc = true
of "failing": optFailing = true
of "pedantic": discard # deadcode refs https://github.com/nim-lang/Nim/issues/16731
of "targets":

View File

@@ -35,6 +35,20 @@ proc bug20303() =
bug20303()
block: # bug #26143
var indexCalls = 0
proc nextIndex(): int =
result = indexCalls
inc indexCalls
proc consume(value: sink string) =
doAssert value == "A"
var values = @["A", "B"]
consume(values[nextIndex()])
doAssert indexCalls == 1
proc main() = # todo bug with templates
block: # bug #11267
var a: seq[char] = block: @[]

View File

@@ -0,0 +1,4 @@
var codegenDeclGlobal* {.codegenDecl: "$# /* custom declaration */ $#".} = 123
proc readCodegenDeclGlobal*(): int {.inline.} =
codegenDeclGlobal

View File

@@ -0,0 +1,5 @@
proc resizeCints*(s: var seq[cint], n: int) =
s.setLen(n)
proc cintLen*(s: seq[cint]): int =
result = s.len

View File

@@ -0,0 +1,13 @@
discard """
output: '''
123
123
'''
ccodecheck: "'extern NI /* custom declaration */ codegenDeclGlobal'"
targets: "c cpp"
"""
import ./mcodegendeclglobal
echo codegenDeclGlobal
echo readCodegenDeclGlobal()

View File

@@ -0,0 +1,15 @@
discard """
action: run
targets: "c cpp"
"""
import mseq_importc_alias
type CIntAlias = cint
var fds: seq[CIntAlias]
doAssert cintLen(@[1.cint, 2.cint]) == 2
doAssert cintLen(fds) == 0
resizeCints(fds, 3)
fds[1] = CIntAlias(7)
doAssert cintLen(fds) == 3

View File

@@ -0,0 +1,20 @@
discard """
action: run
targets: "c cpp"
"""
type CIntAlias = cint
var x: (cint,) = (1.cint,)
var y: (CIntAlias,) = x
x = y
doAssert x[0] == 1.cint
var a: seq[cint]
var b: seq[CIntAlias]
a.add 1.cint
a.add 2.cint
b = a
a = b
doAssert a[0] == 1.cint
doAssert b[1] == CIntAlias(2)

75
tests/concepts/t26147.nim Normal file
View File

@@ -0,0 +1,75 @@
discard """
action: run
"""
type Indexable[T] = concept
proc `[]`(a: Self; index: int): T
proc len(a: Self): int
iterator items[T; I: Indexable[T]](indexable: I): T =
for index in 0 ..< indexable.len:
yield indexable[index]
type Dummy[T] = distinct seq[T]
proc `[]`[T](d: Dummy[T], i: int): T = seq[T](d)[i]
proc len[T](d: Dummy[T]): int = seq[T](d).len
var acc = 0
for x in Dummy(@[1, 2, 3]):
acc += x
doAssert acc == 6
# Inferred concept parameters are resolved through the implementation's own
# generic bindings before being exported to the surrounding routine.
type
Elem[T] = object
value: T
NestedDummy[T] = ref object
data: seq[T]
proc `[]`[T](d: NestedDummy[T], i: int): Elem[T] =
Elem[T](value: d.data[i])
proc len[T](d: NestedDummy[T]): int = d.data.len
iterator directItems[T](indexable: Indexable[T]): T =
for index in 0 ..< indexable.len:
yield indexable[index]
var nestedAcc = 0
for x in NestedDummy[int](data: @[4, 5, 6]):
nestedAcc += x.value
doAssert nestedAcc == 15
var directNestedAcc = 0
for x in directItems(NestedDummy[int](data: @[7, 8, 9])):
directNestedAcc += x.value
doAssert directNestedAcc == 24
# All dependent parameters inferred while checking a concept constraint must
# be propagated to the constrained routine.
type
KeyValue[K, V] = concept
proc key(x: Self): K
proc value(x: Self): V
Pair[K, V] = object
k: K
v: V
proc key[K, V](x: Pair[K, V]): K = x.k
proc value[K, V](x: Pair[K, V]): V = x.v
proc unpack[K, V; P: KeyValue[K, V]](x: P): (K, V) =
(x.key, x.value)
let pair = Pair[int, string](k: 7, v: "seven")
doAssert unpack(pair) == (7, "seven")
doAssert not compiles(unpack[string, int](pair))
proc unpackBoth[K1, V1, K2, V2;
P1: KeyValue[K1, V1]; P2: KeyValue[K2, V2]](
x: P1; y: P2): ((K1, V1), (K2, V2)) =
(unpack(x), unpack(y))
let otherPair = Pair[string, float](k: "eight", v: 8.0)
doAssert unpackBoth(pair, otherPair) == ((7, "seven"), ("eight", 8.0))

View File

@@ -0,0 +1,28 @@
discard """
matrix: "--mm:orc"
output: "destroy b"
"""
# bug #26123
type
A = ptr AObj
AObj = object
b: B
B = distinct ptr BObj
BObj = object
a: A
proc `=destroy`(r: var B) =
echo "destroy b"
proc main() =
var a = create(AObj)
var b = B(create(BObj))
a.b = b
cast[ptr BObj](b).a = a
main()

View File

@@ -166,3 +166,99 @@ type Vector*[T] = object
# proc `=destroy`*(x: var Vector[int]) = discard # this will remove error
proc `=destroy`*[T](x: var Vector[T]) = discard
var a: Vector[int] # Error: unresolved generic parameter
# issue #26132
block:
type UnparameterizedGeneric[T] = object
proc `=destroy`(x: var UnparameterizedGeneric) = discard
proc `=wasMoved`(x: var UnparameterizedGeneric) = discard
proc `=trace`(x: var UnparameterizedGeneric; env: pointer) = discard
var x: UnparameterizedGeneric[int]
discard x
# Exercise every type-bound hook with the generic parameter omitted.
block:
type
Generic[T] = object
value: T
var destroys, moves, traces, copies, sinks, dups: int
proc `=destroy`(x: var Generic) = inc destroys
proc `=wasMoved`(x: var Generic) =
inc moves
x.value = default(typeof(x.value))
proc `=trace`(x: var Generic; env: pointer) = inc traces
proc `=copy`(dest: var Generic; src: Generic) =
inc copies
dest.value = src.value
proc `=sink`(dest: var Generic; src: Generic) =
inc sinks
dest.value = src.value
proc `=dup`(src: Generic): Generic =
inc dups
Generic(value: src.value)
proc deepCopy(src: ref Generic): ref Generic = src
proc exercise[T]() =
var first = Generic[T](value: default(T))
var second = Generic[T](value: default(T))
second = first
doAssert second.value == first.value
second = Generic[T](value: default(T))
doAssert second.value == default(T)
`=trace`(first, nil)
`=wasMoved`(first)
let implicitDuplicate = first
discard implicitDuplicate
let duplicate = `=dup`(first)
discard duplicate
let original = new(Generic[T])
doAssert deepCopy(original) == original
exercise[string]()
exercise[int]()
exercise[seq[int]]()
doAssert copies > 0
doAssert sinks > 0
doAssert dups > 0
doAssert moves > 0
doAssert traces > 0
doAssert destroys > 0
block:
type GenericDistinct[T] = distinct Generic[T]
proc `=destroy`(x: var GenericDistinct) = discard
proc `=wasMoved`(x: var GenericDistinct) = discard
proc `=trace`(x: var GenericDistinct; env: pointer) = discard
proc `=copy`(dest: var GenericDistinct; src: GenericDistinct) = discard
proc `=sink`(dest: var GenericDistinct; src: GenericDistinct) = discard
proc `=dup`(src: GenericDistinct): GenericDistinct = src
proc deepCopy(src: ref GenericDistinct): ref GenericDistinct = src
var first = GenericDistinct[string](Generic[string](value: "first"))
var second = GenericDistinct[string](Generic[string](value: "second"))
second = first
second = GenericDistinct[string](Generic[string](value: "third"))
`=trace`(first, nil)
`=wasMoved`(first)
let moved = move(first)
let duplicate = `=dup`(moved)
discard duplicate
let original = new(GenericDistinct[string])
doAssert deepCopy(original) == original
block:
type GenericPair[A, B] = object
left: A
right: B
proc `=destroy`(x: var GenericPair) = discard
var pair = GenericPair[int, string](left: 42, right: "pair")
discard pair

View File

@@ -0,0 +1,7 @@
proc u(k: static int) =
proc r(_: static int) =
while k > 0:
discard
r(0)
u(0)

View File

@@ -0,0 +1,4 @@
proc pub*(x: int): int = x + 1
proc hidden(): int = 42 # no `*` ...
export hidden # ... but explicitly re-exported

View File

@@ -0,0 +1,4 @@
proc pub*(x: int): int = x + 1
proc secret(): int = 7 # no `*`
proc hiddenToo(x: int): int = x

11
tests/ic/mnestasgn.nim Normal file
View File

@@ -0,0 +1,11 @@
# Helper for tnestasgn.nim: a `sink`-param routine containing a nested proc
# whose ENTIRE body is a single assignment, so the body node is a bare `nkAsgn`
# rather than an `nkStmtList` — the shape that used to be deferred behind a
# childless placeholder of that same kind.
proc consume*(s: sink string) =
var x = ""
proc setIt() =
x = s
setIt()
echo x

48
tests/ic/readme.md Normal file
View File

@@ -0,0 +1,48 @@
# Running `tests/ic`
./bin/testament --nim:<your compiler> cat ic
## The metamorphic tests are expensive, and look hung when they are not
16 of the tests carry `#? metamorphic`. Each has 34 `#!STEP` directives, and
every step compiles the program **twice** — once under `nim ic`, once with
`nim c` as the reference oracle. That is 100+ full compilations for the
category. Under `--ic:on` each compilation additionally fans out one backend
process per module per stage, and each of those is a compiler holding its own
module graph (~800MB peak).
**A `nim ic` parent sitting at 0% CPU is normal.** It is waiting on its
children. It is not a deadlock, and neither is a metamorphic test that occupies
the runner for many minutes. Before concluding anything is stuck, check that the
test NAME changes over a few minutes — that is the difference between slow and
hung, and it is easy to get wrong.
On a memory-constrained machine the fan-out will swap. The symptoms are exactly
the ones that read as a deadlock: several processes at 0% CPU, no output, a
different test "stuck" on every run, and the same compilation finishing in
seconds when run on its own. Check `vm_stat` (page-ins per second) and
`sysctl vm.swapusage` before looking for a bug. This was diagnosed as a
testament/`nim ic` interaction more than once before anyone measured.
Cap the fan-out to fit the machine — precedence documented at `deps.nim`'s
`let parallel`:
--parallelBuild:N # standard flag, given meaning under IC
-d:icJobs:N # same cap, legacy define
-d:icNoParallel # serial, and non-interleaved child output
Serial output matters for a second reason: the parallel backend processes share
one stderr, so any per-process diagnostic printing (`NIM_IC_BNODE_GRIND`,
`-d:icCanRaiseLog`) interleaves and produces torn lines. Either use
`-d:icNoParallel` or parse defensively and count what you dropped.
## Running a single test
`testament r tests/ic/<file>.nim` works for the ordinary tests. It does NOT work
for the metamorphic ones — the multi-step files carry several `discard """`
spec blocks and the single-test path rejects them with "duplicate `specStart`".
Those only run through `cat ic`.
Files matching `tests/ic/*_temp.nim` are ignored by git (see `.gitignore`) and
are scratch, not tests: several import helper modules that do not exist and fail
for that reason alone.

101
tests/ic/tclosure_hooks.nim Normal file
View File

@@ -0,0 +1,101 @@
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

View File

@@ -0,0 +1,90 @@
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

View File

@@ -0,0 +1,34 @@
discard """
description: '''IC: changing the compiler switches must invalidate the cache'''
"""
#? metamorphic
# nifmake decides staleness from file mtimes and never looks at a rule's command
# line, so `-d:` / `--mm:` / `--opt:` changes re-generated the build file with
# the new switches and re-fired nothing: a silently stale binary built with the
# PREVIOUS configuration. And switches given only on the driver's command line
# never reached the per-module children at all, because they replay the
# project's config files rather than the driver's argv.
#!FILE cfg.nim
const Mode* {.strdefine.} = "plain"
proc describe*(): string =
when Mode == "loud": "LOUD"
elif Mode == "quiet": "quiet"
else: "plain"
#!FILE main.nim
import cfg
echo describe()
#!STEP expect: plain
#!FLAGS -d:Mode=loud
#!STEP expect: LOUD
#!FLAGS -d:Mode=quiet
#!STEP expect: quiet
#!FLAGS
#!STEP expect: plain

View File

@@ -0,0 +1,37 @@
discard """
description: '''IC: an import under an undecidable `when` must not be compiled'''
"""
#? metamorphic
# `when SomeStrdefineConst == "x": import y` is `cvUnknown` to the dependency
# scanner, which conservatively keeps the edge — right for an edge, but it also
# gave `y` its own `nim m` rule. `nim c` never looks at that file, so a build
# died on a package the user never installed because they never selected that
# backend. Selecting it must still produce the honest error.
#!FILE needsmissing.nim
import pkg/definitely_not_an_installed_package
proc unreachable*(): string = "never"
#!FILE guarded.nim
const Backend* {.strdefine.} = "plain"
when Backend == "fancy":
import ./needsmissing
proc pick*(): string =
when Backend == "fancy": unreachable()
else: "plain"
#!FILE main.nim
import guarded
echo pick()
#!STEP expect: plain
# selecting the branch that really does need the missing package must report it
#!FLAGS -d:Backend=fancy
#!STEP fails: cannot open file
#!FLAGS
#!STEP expect: plain

View File

@@ -0,0 +1,26 @@
discard """
description: '''IC: deleting a still-imported module must be an error'''
"""
#? metamorphic
# Deleting a file moves no mtime, so nothing in an mtime-keyed build re-fires:
# `nim ic` relinked a stale binary while `nim c` reported `cannot open file`.
# The dependency scan is the only part of the pipeline that looks at import
# paths at all, so that is where the vanished module has to be noticed.
#!FILE helper.nim
proc help*(): string = "helped"
#!FILE main.nim
import helper
echo help()
#!STEP expect: helped
#!DELETE helper.nim
#!STEP fails: cannot open file
# putting it back recovers
#!FILE helper.nim
proc help*(): string = "back"
#!STEP expect: back

View File

@@ -0,0 +1,68 @@
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)"]

View File

@@ -0,0 +1,38 @@
discard """
description: '''IC: a macro-generated import stays in the graph across runs'''
"""
#? metamorphic
# The static scanner cannot see `parseStmt("import dyn")`. The discovery
# fixpoint recovers it — but only ran AFTER a failure, and the graph is
# re-derived statically on every run, so on a warm build the discovered module
# had no nifler/`nim m` rule at all: editing it changed nothing, forever.
#!FILE dyn.nim
proc hidden*(): string = "first"
#!FILE gen.nim
import std/macros
macro generatedImport(): untyped =
parseStmt("import dyn")
generatedImport()
proc reveal*(): string = hidden()
#!FILE main.nim
import gen
echo reveal()
#!STEP expect: first
# the warm build must see this edit
#!FILE dyn.nim
proc hidden*(): string = "second"
#!STEP expect: second
# and again, to prove it is not a one-shot recovery
#!FILE dyn.nim
proc hidden*(): string = "third"
#!STEP expect: third

View File

@@ -0,0 +1,32 @@
discard """
description: '''IC: a failed `nim m` must not poison the cache'''
"""
#? metamorphic
# A `nim m` that errored still wrote its `.s.bif` and cookie sidecars. nifmake
# then saw the rule satisfied (outputs newer than inputs) and the NEXT run
# reported success for a program that does not compile — linking a binary
# generated from error-bearing AST, or crashing codegen outright. Expressing
# this needs a step that is allowed to FAIL and a following step that recovers.
#!FILE dep.nim
proc value*(): int = 41
#!FILE main.nim
import dep
echo value() + 1
#!STEP expect: 42
# introduce a real error
#!FILE dep.nim
proc value*(): int = undefinedThing() + 1
#!STEP fails: undeclared identifier: 'undefinedThing'
# ... and again: the second run must NOT decide the rule is up to date.
#!STEP fails: undeclared identifier: 'undefinedThing'
# fixing it must rebuild rather than serve the poisoned artifact
#!FILE dep.nim
proc value*(): int = 100
#!STEP expect: 101

View File

@@ -0,0 +1,14 @@
discard """
output: '''42'''
"""
# `export s` re-exports a symbol whose declaration has no `*`. It reaches the
# module interface through `reexportSym` alone, so a NIF writer that decides
# importability from `sfExported` ships it as private and the importer reports
# "undeclared identifier". `std/random` does exactly this
# (`proc initRand(): Rand` + `since (1, 5, 1): export initRand`), which made
# `--ic:on` unable to compile anything reaching `std/tempfiles`.
import mexportprivate
echo hidden()

View File

@@ -0,0 +1,49 @@
discard """
description: '''IC vs `nim c`: module-level globals must be destroyed at exit'''
"""
#? metamorphic
# `graph.globalDestructors` is filled while a module's top level goes through
# `injectDestructorCalls`, and whole-program cgen empties the list into the main
# module's init proc — which IS the program body, so the calls land at program
# exit. Under `nim ic` every module's `cg` is a separate process, so the main
# module's `cg` only ever saw its OWN entries and a module-level `var` with a
# `=destroy` in any imported module was simply never destroyed.
#
# The teardown ORDER is the other half: it must be the reverse of the init order
# (importers before their dependencies), which is what the oracle pins down here
# — three modules in a chain plus main, each with a global of its own.
#!FILE gdlog.nim
type G* = object
tag*: string
proc `=destroy`*(g: G) = echo "destroy ", g.tag
proc mk*(t: string): G = G(tag: t)
#!FILE gda.nim
import gdlog
var ga* = mk("a")
#!FILE gdb.nim
import gdlog, gda
var gb* = mk("b:" & ga.tag)
#!FILE gdc.nim
import gdlog, gdb
var gcv* = mk("c:" & gb.tag)
#!FILE main.nim
import gdlog, gda, gdb, gdc
var gmain = mk("main")
echo "body ", ga.tag, " ", gb.tag, " ", gcv.tag, " ", gmain.tag
#!STEP
# touching a leaf module must not lose anyone's teardown
#!FILE gda.nim
import gdlog
var ga* = mk("a2")
#!STEP

View File

@@ -0,0 +1,7 @@
import ../ccgbugs/mseq_importc_alias
type CIntAlias = cint
var values: seq[CIntAlias]
resizeCints(values, 2)
doAssert cintLen(values) == 2

View File

@@ -0,0 +1,17 @@
discard """
output: '''42'''
"""
# `import x {.all.}` makes x's PRIVATE symbols visible. Under IC that means the
# hidden half of a loaded module's interface has to be there — and it is now
# built on demand rather than at load time, because almost nothing ever reads it
# (1.70M hidden stubs against 0.29M exported ones on a cold Atlas build).
#
# The trap the first attempt fell into: a module has TWO FileIndexes. `c.mods`
# in the decode context is keyed by the one `registerNifSuffix` mints for the
# NIF suffix; `g.ifaces` is indexed by the module's source file. Asking one with
# the other misses silently, and this test is what says so.
import mimporthidden {.all.}
echo secret() + hiddenToo(35)

18
tests/ic/tnestasgn.nim Normal file
View File

@@ -0,0 +1,18 @@
discard """
output: '''hi'''
"""
# Regression test, minimized from a nimbus-eth2 `nim ic` crash by
# https://github.com/nim-lang/Nim/pull/26106 (the only one of that PR's eight
# repros that reproduces on its own base).
#
# A NIF-loaded routine's body is installed as a `nfLazyBody` placeholder. The
# placeholder used to carry the REAL body kind while holding no children, which
# breaks the compiler's most basic invariant — a node's kind implies its arity.
# `trees.getPotentialWrites` walks the outer routine because it has a `sink`
# parameter, reaches the nested proc's body under `of nkAsgn`, and reads
# `n[0]`/`n[1]` as every such reader is entitled to: "index out of bounds, the
# container is empty [IndexDefect]".
import mnestasgn
consume("hi")

View File

@@ -0,0 +1,20 @@
discard """
output: '''caught'''
"""
type
Base = ref object of RootObj
Child = ref object of Base
method run(value: Base): string {.base.} =
result = "base"
method run(value: Child): string =
raise newException(ValueError, "child")
let value: Base = Child()
try:
discard value.run()
quit "virtual method did not raise"
except ValueError:
echo "caught"

View File

@@ -11,6 +11,9 @@ proc fn2[T](a: var openArray[T]): seq[T] =
proc fn3[T](a: var openArray[T]) =
for i, ai in mpairs(a): ai = i * 10
proc wr[T](a: var openArray[T]; v: T) =
a[0] = v
proc main =
var a = [1,2,3,4,5]
@@ -20,8 +23,22 @@ proc main =
doAssert fn2(a.toOpenArray(1,3)) == @[2,3,4]
fn3(a.toOpenArray(1,3))
when defined(js): discard # xxx bug #15952: `a` left unchanged
else: doAssert a == [1, 0, 10, 20, 5]
doAssert a == [1, 0, 10, 20, 5]
block: # bug #15952: `toOpenArray` slices are live views on JS
# Fixed homogeneous numeric arrays lower to JS typed arrays; seqs and
# non-numeric fixed arrays lower to plain JS arrays. In all cases a slice
# passed to a `var openArray` must alias the source so writes propagate
# (JS: subarray view for typed arrays, {base,off,len} view otherwise).
var si = @[1, 2, 3, 4, 5]
fn3(si.toOpenArray(1, 3))
doAssert si == @[1, 0, 10, 20, 5]
var ss = ["a", "b", "c", "d", "e"]
wr(ss.toOpenArray(1, 3), "Z")
doAssert ss == ["a", "Z", "c", "d", "e"]
# read-only slicing must still work and never throw, on every backend.
doAssert fn1(@[1, 2, 3, 4, 5].toOpenArray(1, 3)) == @[2, 3, 4]
doAssert fn1(["a", "b", "c", "d", "e"].toOpenArray(1, 3)) == @["b", "c", "d"]
block: # bug #12521
block:

24
tests/stdlib/t26134.nim Normal file
View File

@@ -0,0 +1,24 @@
discard """
matrix: "--mm:orc --undef:nimPreviewNonVarDestructor"
output: "hello"
"""
# bug #26134
type MyObject = object
proc `=destroy`(v: var MyObject) =
echo "hello"
proc remove(v: var seq[MyObject]) =
v.del(0)
proc aaa(v: var seq[MyObject], i: sink MyObject) =
v.add(i)
proc main =
var v: seq[MyObject]
v.aaa(MyObject())
v.remove()
main()

Some files were not shown because too many files have changed in this diff Show More