In the per-module model each module's init/datInit proc is generated by its
own cg process, but NimMain (emitted with the main module) must call them all.
Previously the main module's cg only registered its own init, so a/b's globals
stayed at their defaults -- the linked exe ran but printed wrong values.
When the cg target is the main module, register every other loaded module's
init/datInit into NimMain via registerReusedModuleToMain (exported), reading
each module's initRequired/datInitRequired from its .c.nif meta head. This is
the same no-codegen registration the whole-program backend uses for reused
TUs; it requires the main module's cg to run last, after every other .c.nif
exists (the per-module nifmake graph will order it so). Modules without init
code have no .c.nif and register nothing.
Validated: the 3-module diamond now builds end-to-end through the per-module
pipeline (cg all with main last, merge, emit all, cc, link) and the resulting
executable prints the correct "16 23" -- matching the whole-program build.
koch ic thallo green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consts and RTTI are demand-emitted, so under emit-everywhere `cg` the same
external-linkage data definition lands in several modules' .c.nif as `cdata`
(raw text, not droppable) -> multiple definition at link. Procs already
deduped via the `'u'` cdef flag; data now gets the same droppable+owner
treatment, with one difference: data is never DCE'd (RTTI needs pointer
identity for `of`/exception checks; static-per-TU would break that), so it is
always a liveness root and kept by its single owner regardless of liveness.
- New `'d'` cdef flag = a data definition: the merge stage assigns it one
owner (smallest claimant, like `'u'`) and roots it (so its body keeps the
procs it references live); emit keeps the body only in the owner, every
other module keeps just an always-emitted `extern` declaration (the data
analogue of a proc prototype).
- genConstDefinition (ccgexprs) and genTypeInfoV2Impl (ccgtypes) now, under
cmdNifC, emit an extern declaration + wrap the definition in a `'d'` cdef
directive. The RTTI forward decl becomes a real `extern` (was a tentative
definition that would collide across TUs).
- cnif: computeLiveFromCArtifacts, computeMergeDecision and
renderCFromArtifact all handle `'d'`.
icFormatVersion 4 -> 5 (old .c.nif lack the data wrappers).
Validated on the 3-module diamond: the full per-module pipeline (cg all,
merge, emit all, cc, link) now LINKS with no duplicate symbols -- RTTI
(NTIv2) and const tables each land in exactly one object. Whole-program IC
path unchanged (koch ic thallo/tconverter/tmiscs green). Remaining: NimMain
init orchestration (a/b module inits not yet called from main's cg -> the
linked exe runs but prints defaults), the next unit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
--icBackendStage:emit --icBackendModule:<suffix> renders one module's final
.c from its .c.nif and ic.backend.merge.nif. cnif.renderCFromArtifact walks
the artifact token stream: string literals verbatim, symbols by name, and a
(cdef ...) body is dropped when the name is dead OR it is a 'u' unique
definition this module does not own. The prototype lives in the surrounding
raw text (cgen emits a forward declaration for every used proc regardless of
where the body lands), so a dropped body keeps a valid declaration -- no
synthesis needed.
emit loads the module graph the same way cg does (factored into
loadBackendModules/findTargetModule) so getCFile yields the identical path cg
wrote to -- in particular the main module's source-vs-suffix aliasing.
Validated end-to-end on a 3-module diamond (lib.shared demanded by siblings
a and b at top level): cg all modules, merge, emit all, cc, link. The proc
shared lands in exactly one object (its assigned owner a) and is referenced
(U) from the other -- proc dedup + ownership works at the object level. The
only remaining link failures are DATA (RTTI NTIv2, const tables): those are
emit-everywhere'd as cdata, which is not yet wrapped in a droppable directive
nor given a guaranteed extern in non-owners -- the next unit (data ownership).
koch ic thallo green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorder mangleProcNameExt and makeUnique so the module suffix comes LAST:
name_u<disamb>__<suffix> (was name__<suffix>_u<disamb>). The suffix is now a
strippable trailing token, so content-addressed cross-module merging (the
per-module backend's instance/hook dedup) can recover a mint-site-independent
name by chopping everything from the final "__" -- no reference rewriting.
Also drops the main-module special case in mangleProcNameExt: it omitted the
suffix because the main module's symbols key on its NIF-suffix file index. But
the backend already aliases that suffix to the main's source index
(nifbackend.loadModuleDependencies), so graph.ifaces[s.itemId.module] is
populated for the main module too -- the guard was redundant. Main-module
procs now mangle uniformly (e.g. mainProc_u0__<mainname>).
icFormatVersion 3 -> 4: cached .c.nif artifacts hold the old name scheme and
must be wiped.
Validated: koch boot (non-IC self-host) reaches fixed point; koch ic thallo
tconverter timp tmiscs tparseutils all green; a 3-module diamond IC build
runs correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
--icBackendStage:merge reads every module's .c.nif (the cg stages'
emit-everywhere output), computes the global live set and, for each
externally-linked definition that several cg processes emitted, the single
artifact allowed to embed its body; it writes ic.backend.merge.nif for the
emit stage to consume. This is the cross-process replacement for the
whole-program backend's in-process icSharedDefOwner/DCE coordination.
Mechanism:
- cgen marks every unique program-wide definition (callConv != ccInline and
not a dispatcher) with a new 'u' cdef flag. Its complement -- inline procs
(static per-TU) and method dispatchers (main-only) -- is emitted into every
using TU and must never be deduplicated, so it carries no flag. The flag is
inert for the whole-program path (renderMarkedC/computeLiveFromCArtifacts
ignore it).
- cnif.computeMergeDecision does one mark&sweep pass over all artifacts
(same liveness as computeLiveFromCArtifacts) plus owner assignment: the
owner of a 'u' definition is the lexicographically smallest artifact that
emits it -- a pure function of the claimant set, stable across rebuilds.
writeMergeDecision/readMergeDecision serialize the result as
(merge (live ...) (owners (own Symbol StrLit)*)).
- generateMergeStage is a pure artifact operation (no module graph loaded):
glob the nimcache's .c.nif, compute, write the decision.
Validated on a diamond (lib.shared called from sibling modules a and b, both
with top-level demands): cg emits shared into a, b and lib; merge assigns
owner = lib (smallest claimant) so a/b will prototype it, while the inline
nimFrame stays out of the owners map (kept everywhere). Whole-program backend
path unchanged (dispatch guarded on icBackendStage); koch ic thallo green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
--icBackendStage:cg --icBackendModule:<suffix> generates C for a single module
and writes only its .c.nif (no merge, no .c render, no cc/link -- separate
stages). The whole program is still loaded so types resolve, but only the
target module is code-generated; findPendingModule routes every demand into it
(emit-everywhere into the current module), so a definition gets its canonical
owner-suffixed C name regardless of which module's process emits it -- cross-
process duplicates then collide by exact name, ready for the merge stage to keep
one and prototype the rest.
Validated: cg of the main module of a 2-module project recreates its .c.nif with
the demanded closure (greet/add named by their owner suffix); a leaf module whose
procs only callers use yields an (correct) empty .c.nif. Whole-program backend
path unchanged (dispatch guarded on icBackendStage), koch ic thallo green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Config plumbing for the per-module backend: icBackendStage (cg|merge|emit, empty
= today's whole-program backend) and icBackendModule (the NIF suffix the cg/emit
stage operates on). No behavior yet -- the nifbackend stages and deps.nim rules
that consume these land next. Whole-program backend, koch boot, and a 2-module
IC build are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
generateBuildFile becomes three procs: computeForwardedArgs (config/define
forwarding + writeIcConfig, depends only on config so computed once),
generateFrontendBuildFile (nifler + nim m rules), and generateBackendBuildFile
(today's single whole-program nim nifc rule; semmed NIFs enter as leaf inputs
with no producing rule, like nifler's .nim source inputs).
commandIc now runs two nifmake passes: phase 1 drives the frontend to the
.s.deps discovery fixpoint, phase 2 runs the backend once over the now-final
graph. Backend rebuilds are then a pure nifmake mtime decision, independent of
frontend discovery -- and the backend file is the slot the per-module codegen
+ DCE + link rules drop into next.
Observably inert: koch boot and koch bootic both reach their byte-identical
fixed points (clean ric_ cache), 2-module cold/warm/body-edit correct,
koch ic thallo green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The content key is hashed into <disamb> (setInstanceDisamb), not a separate
.key. token; document that the cross-TU merge and DCE already key on the
module-suffix-stripped name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tests the fix in 7148ae347: the RHS of a dot expression wrapped in
nkOpenSym by the generic prepass must use the captured symbol when nothing
is injected, while an injected symbol still overrides it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cold builds were serial (one nim m at a time), leaving the machine idle.
nifmake fans out commands at each DAG depth via execProcesses, so pass
--parallel by default; this roughly halved cold compiler self-build wall
time (81s -> 53s on a 32-core box). Opt out with -d:icNoParallel for
readable, non-interleaved child output when debugging a build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An UncheckedArray has no known length, so it cannot be copied, moved or
destroyed as a value; it only ever lives behind a pointer. The pointer-like
group emitted x = y for it, an assignment of an unsized array the C backend
cannot lower (genAssignment: tyUncheckedArray) -- which surfaced under
nifc's hook-driven refc codegen (e.g. ref UncheckedArray in widestrs).
Give it its own discard branch so all value hooks no-op; seq/string element
ops still go through the seq/string hooks, which know the length.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The early IC fix made the else branch (concrete generic args) copy the
invocation type unconditionally before propagateToOwner. Besides
avoiding the in-place mutation, the copy flips `header != t` for
all-concrete invocations, which activates the searchInstTypes/sameFlags
cached-instance return path that devel skipped - a cached, meta-flagged
instance could be returned where a fresh one was expected.
Arraymancer's build then failed with "cannot cast to a non concrete
type: 'ptr NimSeqV2[Node[Tensor[float32]]]'" in seqs_v2.setLen.
Copy only when the invocation type is actually immutable
(IC-loaded/Sealed); non-IC behavior is devel's again, the IC assert
stays fixed.
Verified: arraymancer tests_cpu.nim builds and links (its test-suite
SIGSEGV in io_npy is pre-existing - a devel-built compiler produces the
identical 226-tests-then-crash). Macro sweep 93/95, tests/ic 5/5,
koch boot -d:release and clean koch bootic reach bit-identical fixed
points.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a generic's body is computed by a macro (Bar[T, U] = makeBar(T, U)),
`newbody` after replaceTypeVarsT can be a type loaded from a dep module
- even a builtin like `int` - which is Sealed under IC:
- the in-place flag accumulation `newbody.flags = newbody.flags + ...`
asserted (and under non-IC silently pollutes the shared type's flags,
e.g. the global `int`); compute the flags into a local, skip the
in-place store for Sealed types and feed `result.flags` from the
local - value-identical for the instance.
- `newbody.typeInst = result` likewise; a loaded body keeps whatever
its defining module serialized (the field was first-wins anyway).
Both changes are no-ops for non-IC (types are never Sealed there).
Fixes tmacrogenerics. Macro sweep 93/95 - the two remaining fails are
tmacro7 (disabled test, fails identically under non-IC) and
tmacrogetimpl (needs a design decision on getImplTransformed sym
sharing). tests/ic 5/5, koch boot -d:release and clean koch bootic
both reach bit-identical fixed points.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under `nim nifc` the main module is loaded by its source FileIndex, but
its serialized symbols carry the module's NIF suffix, so
registerNifSuffix allocated a SECOND FileIndex for the same module:
top-level globals were emitted into the source-index BModule while
procs went into the suffix-index BModule, and a N_LIB_PRIVATE global
declared in one TU was undeclared in the other (tincremental,
tmacros_various).
Pre-aliasing the suffix to the source index in loadModuleDependencies
unifies the TUs. This was tried before and reverted: the split was
masking a hook C-name disamb collision between sem-lifted (loaded) and
codegen-lifted hooks in the same module. That collision class is gone
since backend-minted symbols mangle as _c<item> (BackendIdOffset), so
the unification is safe now.
Macro sweep 92/95 (fixes tincremental + tmacros_various; remaining:
tmacro7 which fails identically under non-IC and is disabled,
tmacrogenerics, tmacrogetimpl), tests/ic 5/5, clean koch bootic
reaches the bit-identical fixed point.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `pkg marker appended to skPackage NIF names leaked into the
user-visible name of Partial stubs: vmgen's toKey built callback keys
like 'getCurrentException.system.stdlib`pkg', so the VM compiled the
real body of getCurrentException instead of dispatching to the vmops
callback and failed with 'cannot evaluate at compile time:
currException' (tparsefile, ttryparseexpr). The marked name was also a
latent hash-divergence source: sighashes' hashNonProc/hashOwner hash
package names straight off possibly-Partial stubs.
Stubs are now created with the clean name; the marker doubles as a kind
signal, so the stub starts as skPackage and globalName re-appends the
marker when rebuilding the NIF index key. NIF file content is
unchanged.
Macro sweep 90/95 (up from 88, restores the baseline; the 5 remaining
fails are the known deep ones), tests/ic 5/5.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ast2nif.canonicalRoutine: collapse a forward-decl's discarded impl sym
onto the surviving proto so it is not serialized twice (was an ambiguous
overload in importers; fixes tnewlit).
- deps.nim: handle `import m except syms` (importexcept) in the dependency
scanner so the build-order edge is not dropped (fixes strformat->strutils
ordering).
- ast2nif.writeSymDef: object fields (skField) are no longer marked
bare-importable (x) in the NIF index; an exported field name leaked into
importer scope and shadowed a template's open symbol (type mismatch 'T').
Together these fix tmacro8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fixes#22936
This pull request improves the compiler's handling of generic type
constraints, specifically for subtypes of generics, and adds a test to
cover this behavior. The main changes are an enhancement to the type
relationship logic in the compiler and a new test case for generic
subtyping with `Future`.
### Compiler improvements for generic subtyping
* Updated `typeRel` in `compiler/sigmatch.nim` to allow generic
constraints (like `F: Future`) to accept not just direct instantiations
but also descendants of the generic family, ensuring more flexible and
correct overload resolution. Inheritance depth is now considered for
overload ranking, making deeper descendants slightly less preferred,
consistent with other inheritance-based matches.
### New test coverage
* Added a test in `tests/typerel/t8905.nim` to verify that generic
constraints correctly accept subtypes of `Future`, including a custom
`B[T, E] = ref object of Future[T]` type, and that overloads like
`take`, `takeMany`, and the macro `checkFutures` work as expected with
these types.
fixes #20811
This pull request addresses issues with parameter capture in nested
generic procedures and templates, ensuring that outer parameters are
correctly visible and accessible within nested scopes. The main changes
include a fix in the semantic analysis logic and the addition of
targeted regression tests.
### Semantic analysis improvements:
* Updated `semGenericStmtSymbol` in `compiler/semgnrc.nim` to ensure
that parameters from outer scopes are preserved and accessible in nested
generic procedures, fixing visibility issues with captured parameters.
### Added regression tests:
* Added `tests/generics/t20811.nim` to verify that both generic and
plain inner procedures can access parameters from their enclosing
procedure.
* Extended `tests/template/topensym.nim` with a new block for issue
#20811 to test that template-injected parameters are correctly captured
and visible in nested generic procedures.
fixes#18238
This pull request makes a targeted change to the object construction
logic in the `genObjConstr` procedure. The main update refines the
conditions under which memory zeroing is required during object
construction, making the behavior more accurate for different garbage
collection and destructor options.
Key logic update:
- Improved the `needsZeroMem` condition in `genObjConstr` to check for
the presence of garbage-collected references and the `optSeqDestructors`
option, instead of relying solely on the selected garbage collector and
field flags. This ensures memory is zeroed only when necessary,
potentially improving performance and correctness.
```c
T1_ = NIM_NIL;
T1_ = ((tyObject_E__uEKympBdEK4SY9anUbpNaLQ*) newObj((&NTIrefe__bJ9cSuxv8xHYxmdolQqFkUw_), sizeof(tyObject_E__uEKympBdEK4SY9anUbpNaLQ)));
nimZeroMem(((void*) ((&(*T1_).z.z.z.z))), sizeof(tyObject_A__G2lWlL9cFqoiWWwZmWqfJ9bA));
(*T1_).z.z.z.z.y = ((NI) 5);
asgnRef(((void**) ((&z1__test8_u12))), T1_);
asgnRef(((void**) ((&z2__test8_u55))), new__test8_u13());
(*z2__test8_u55).z.z.z.z.y = ((NI) 5);
T2_ = NIM_NIL;
T2_ = ((tyObject_E__uEKympBdEK4SY9anUbpNaLQ*) newObj((&NTIrefe__bJ9cSuxv8xHYxmdolQqFkUw_), sizeof(tyObject_E__uEKympBdEK4SY9anUbpNaLQ)));
asgnRef(((void**) ((&z3__test8_u56))), T2_);
(*z3__test8_u56).z.z.z.z.y = ((NI) 5);
```
The original test case has already been fixed for `ORC`, now extends it
to `refc`: if a constructor is fully initialized, it does not need a
zero-fill step
fixes#25725
This pull request makes significant improvements to symbol handling
during transformation passes in the compiler, particularly for routines
(procedures, iterators) and their parameters. The changes ensure that
when routines are copied (for inlining, closure generation, etc.), all
relevant symbols and type headers are also freshly copied and correctly
owned, preventing subtle bugs from symbol reuse. Additionally, new
regression tests are added to cover previously problematic iterator
cases.
**Improvements to symbol copying and ownership:**
* Introduced `freshOwnedSym` to create a fresh copy of a symbol with a
specified owner, ensuring that transformed routines and their parameters
do not share symbols with the originals, which prevents accidental
aliasing and ownership issues.
* Refactored `freshVar` to use `freshOwnedSym`, centralizing fresh
symbol creation logic.
* Added `introduceNewRoutineHeaderSyms` and `copyRoutineTypeHeader` to
ensure that when routines are copied, all parameter/result symbols and
their types are also freshly copied and mapped, avoiding shared state
between original and transformed routines.
* Updated `introduceNewLocalVars` to use `freshOwnedSym` for routine
symbols and to invoke the new header/type copying procedures, ensuring
correctness in routine transformation.
**Testing and regression coverage:**
* Added new blocks to `tests/iter/titer_issues.nim` to test iterator
transformation edge cases, including scenarios that previously led to
symbol reuse bugs (e.g., bugs #25724 and #25725).
Encountered in realistic scenario. Didn't really look at this one. AI
one shot it lol
When a module defines method-bearing types and a when isMainModule
block imports additional modules that also define methods on the same
type hierarchy, sortVTableDispatchers crashes with:
Error: unhandled exception: key not found: (module: N, item: M)
[KeyError]
Root cause: the itemTable built during vtable sorting is populated
from g.objectTree[baseType], which only contains types from the
current compilation pass. When when isMainModule triggers re-import
of method-bearing modules, the method bucket contains types from both
passes. Types from the first pass have ItemIds not present in the
second pass's object tree, so itemTable[obj.itemId] raises KeyError
at line 155.
Fix: if obj.itemId is missing from itemTable, create an empty slot
array of the correct length. The entry is a local temporary — the
second loop in sortVTableDispatchers only calls setMethodsPerType
for types in the current object tree, so types from the prior pass
retain their already-established dispatch. The entry exists solely to
prevent the KeyError during the assignment loop.
The methodIndexLen used for the new entry is the bucket's slot count,
which is correct for any type in the hierarchy.
Added test tests/method/tvtable_reentry.nim that defines methods
across three types in two compilation passes and verifies dispatch
correctness for all three.
ref https://github.com/nim-lang/Nim/issues/25849
The important part is in compiler/vmgen.nim:1838: when the VM lowers
a[i] or a.b as an address-producing operation, it emits opcLdArrAddr /
opcLdObjAddr. That returns an alias into the storage owned by the source
register. Before the patch, that source register could still betreated
as a normal temporary and later reclaimed or reused by the allocator.
Once that happened, the address result was still live, but the backing
temp was no longer guaranteed to exist, which is what led to the
nil/illegal-storage crash.
The fix is to pin that source temp by changing its slot kind to
slotTempPerm right after emitting the address load. You can see the same
lifetime rule already existed for the generic addr(...) path around
compiler/vmgen.nim:1551: if the source is a temporary and we take its
address, the compiler marks it permanent so freeTemp won’t recycle it.
The patch extends that exact rule to array and object address loads:
- compiler/vmgen.nim:1843
- compiler/vmgen.nim:1861
slotTempPerm is outside the normal freeTemp range in
compiler/vmgen.nim:248, so once a temp is upgraded to permanent, the VM
allocator stops treating it as reusable. That is the actual root-cause
fix: it preserves the backing storage for the address result until the
surrounding evaluation is done.
The regression test in tests/vm/t25849.nim:8 forces exactly that path
with a local lent iterator over an array and a static VM evaluation.
fixes#25595
## Bug
A `let` bound to a field of a value-type **case object** with a `ref`
field is inferred as a non-owning cursor, but the cursor's source can be
mutated through the cursor's own ref during a call, freeing the ref
while the borrow still reads it. Use-after-free under arc/orc (refc is
unaffected, it has no cursor inference):
```nim
var destroyed = false
type
O = ref object
value: int
home: H
W = object
case k: bool
of true: r: O
of false: discard
H = ref object
w: W
proc `=destroy`(o: var typeof(O()[])) =
destroyed = true
proc clear(o: O): int =
o.home.w = W() # overwrites h.w via the back-reference -> frees the ref
doAssert not destroyed # fails: the element was destroyed during the call
result = o.value
proc go(h: H): int =
let c = h.w # inferred cursor (borrow of h.w)
result = clear(c.r)
proc main =
let h = H()
let o = O(value: 42)
o.home = h
h.w = W(k: true, r: o)
doAssert go(h) == 42
main()
```
The `not destroyed` assert fails: the element is destroyed during the
call, so the following `o.value` read is a use-after-free. The same code
with the `=destroy` guard removed (so the freed `o.value` is actually
read) is reported as `heap-use-after-free` by ASan under `-d:useMalloc
-fsanitize=address`. Longstanding (reproduces back to 2.2.0).
`--cursorInference:off` is a workaround.
## Root cause
Cursor inference (`varpartitions.computeCursors`) cursors `let c = h.w`
unless `dangerousMutation` finds a mutation of `c`'s graph within `c`'s
alive range `aliveStart..aliveEnd`. Here the mutation (the `clear(c.r)`
call) *is* connected to `c`'s graph and *is* recorded with `isMutated`,
but it is recorded at an `abstractTime` just past `c.aliveEnd`, so the
range check misses it.
The gap is timing. `aliveEnd` is set from the last `nkSym` use of `c`. A
call records its argument's mutation *after* traversing the whole
argument subtree (`potentialMutationViaArg`). When the argument is `c.r`
on a case object it is an `nkCheckedFieldExpr` (the discriminant check),
whose extra nodes advance `abstractTime` past `c`'s last `nkSym`. A
plain `nkDotExpr` has no such gap, so the bug needs a case object.
## Fix
In `potentialMutation`, extend the mutated variable's liveness to the
mutation time:
```nim
v.s[id].aliveEnd = max(v.s[id].aliveEnd, v.abstractTime)
```
A variable mutated at time T is provably alive at T, so this only
completes the liveness computation that `dangerousMutation` relies on.
The worst case is an extra copy, never an unsound cursor.
## Note on the locus
The fix is conservative by mechanism (it runs at every recorded
mutation) but perf-neutral in practice: it only suppresses a cursor
where the corrected liveness proves the borrow unsafe (cursor counts are
unchanged on the suites). I can scope it to call arguments if you'd
prefer it narrower.
## Test
`tests/arc/t25595.nim`, matrix `--mm:orc; --mm:arc; --mm:refc`: the
repro above as a `doAssert`. Fails (UAF) on arc/orc before the fix and
passes after. refc passes throughout.
## Checks
- repro passes on orc/arc after the fix. The guard-removed variant
(which reads the freed value) is ASan-clean after the fix and was
heap-use-after-free before. refc unaffected.
- testament `destructor` 90/90, `arc` 120/120. `views` 5/6, same as
stock (the one failure is environmental and pre-exists this change).
- perf-neutral: inferred-cursor count is identical stock vs fix across
the `arc` and `destructor` test files under `--mm:orc` (322 vs 322).
fixes#25857
## Bug
`typeof(result)` inside the expression that builds `result` gets counted
as a read
of `result` before it's set. On a `{.requiresInit.}` return type that's
a hard error
("'result' requires explicit initialization"). `typeof` never evaluates
its operand,
so it's a false positive. On 2.2.4 it compiles, but the same line still
emits a bogus
`ProveInit` warning, so no released version gets it right.
Regression from #25151. That PR made a used-before-init `requiresInit`
result a hard
error instead of a warning, which is correct on its own. The side effect
was that
this old false-positive warning became a build error.
## Root cause
`track` in `compiler/sempass2.nim` has no arm for `nkTypeOfExpr`, so it
hits the
default that recurses into every child, reaches the `result` `nkSym`
inside the
`typeof`, and calls `useVar`. `sizeof`/`compiles`/`declared` don't hit
this because
they fold to a constant before `track` runs. A `typeof(result)` typedesc
argument
survives into `track`.
## Fix
Skip `nkTypeOfExpr` in `track`. Its operand is never evaluated, so it
isn't a
definite-assignment use. After the patch there's no error and no warning
here, even
with `--warnings:on`. The #25151 check is untouched: a real use of
`result` before
init is a plain `nkSym`, not inside a `typeof`, so it still reaches
`useVar`.
## Test
`tests/init/t25857.nim`, a positive test that compiles and prints `1`.
## Checks
- Repro compiles and runs on patched 2.2.6 and patched devel.
- `tests/errmsgs/t25117.nim` still fails as expected. A real
`xxx(result)` before
init still errors.
- `testament cat init` and `testament cat errmsgs` green on patched
devel (55 tests,
0 failures), including the `--warningAsError:ProveInit` tests.
- Bisect: parent `1ab68797` good, `576c4018` (#25151) bad.