Compare commits

..

49 Commits

Author SHA1 Message Date
narimiran
c5bf6d55d6 stupid commit to please github 2026-06-23 14:48:17 +02:00
ringabout
6eef0cc2d5 fixes #25908; resolves lent enum disambiguation (#25929)
fixes #25908

When an enum identifier is resolved as an `nkSymChoice`, one of the
candidates may come from a loop-local view and carry `tyVar` or
`tyLent`.

Enum disambiguation should compare the underlying enum type only.
Otherwise a pure-enum field can win incorrectly even though the intended
symbol is already present in the choice set.

Keep the `includePureEnum` lookup path for enum-typed expectations so
#23976 still works, but normalize `var`/`lent` only at the symchoice
selection point.
2026-06-23 12:22:29 +02:00
Ryan McConnell
f8e470eb57 fix: {.cast(uncheckedAssign).} ineffective across yield in closure iterators (#25916)
closureiters.nim splits a stmt list at yield points, moving post-yield
code into a new state body. When that stmt list was inside a pragma
block like `{.cast(uncheckedAssign).}`, the new state's body was created
as a bare nkStmtList without the wrapper.

Fix: track the enclosing pragma block in the transform context, and wrap
newly-created state bodies in a copy of it when the split occurs inside
one. Added an explicit `nkPragmaBlock` case to
`transformClosureIteratorBody` that saves/restores `ctx.enclosingPragma`
around its body.
2026-06-16 19:15:16 +02:00
Andreas Rumpf
f5d9e7a207 IC: bugfixes (#25914) 2026-06-15 23:33:16 +02:00
Andreas Rumpf
acb9b3a4f1 IC: precompiled configs and bugfixes (#25913) 2026-06-15 21:20:09 +02:00
Andreas Rumpf
7171e6f01f IC: progress (#25879) 2026-06-14 22:35:06 +02:00
Andreas Rumpf
9d7c0cc683 SSO: add readRawDataStable across all string implementations (#25909)
Companion to readRawData whose pointer stays valid across moves/copies
of the string. Under --strings:sso it promotes a small inline string to
its heap representation; under refc/v2 the data is already heap-resident
so it aliases readRawData.

Uniform `var string` signature on every backend so code can prepare for
--strings:sso without `when declared`.
2026-06-13 19:27:22 +02:00
Aleksei Rybnikov
c292ab987b docs: correct the Delegating bind statements example (fixes #19240) (#25890)
Fixes #19240.

The Manual's "Delegating bind statements" example didn't compile (module
B didn't import A, type `O` wasn't exported, and `x: T` couldn't bind to
`var O`), and once those were fixed it compiled *without* the `bind`
statement — so it didn't demonstrate delegating bind at all.

This replaces it with a minimal example that genuinely requires `bind
init`: `module main` imports A and B but not C, so `init` is not in
scope at the final instantiation of `genericA`; the open `mixin` symbol
fails to resolve without `bind init` forwarding it from module B.
Verified to fail without `bind` and compile with `bind` under Nim
2.2.10.

---
Disclosure: I work with Claude as a co-processor. I understand what I'm
submitting and I verified the example against the compiler myself. If
you prefer human-only contributions, just say so and I'll close without
friction.
2026-06-13 12:54:19 +02:00
ringabout
587f90a816 fixes #22122; Unclear error message for raise of a complex expression (#25899)
fixes  #22122

The commit fixes a bug in Nim's effects checker where raise statements
with case/if expressions (commonly from template expansion) failed to
track exception types from individual branches.
Problem: addRaiseEffect only saw the outermost expression. When a
template like getTransportError(err) expanded to a case expression
raising 3 different exception types, the compiler only registered the
top-level call — missing the branch-level exceptions.
Fix (2 files):
- compiler/sempass2.nim: Added skipHiddenConv to strip implicit type
coercion nodes (nkHiddenStdConv/nkHiddenSubConv) that hide the control
flow structure. Added addRaiseEffectsFromExpr that recursively walks
into case/if/block/stmtlist expressions to find raise effects in each
branch body. Changed the nkRaiseStmt handler to use this new function.
- tests/effects/tcase_raises.nim: Test with templates that expand to
case expressions raising different exception types, verified via
{.raises: [].} pragma.
2026-06-13 10:17:59 +02:00
ringabout
8ad1d106ec fixes #25885; incompleteStruct ignored without importc (#25898)
fixes #25885
2026-06-13 10:17:44 +02:00
ringabout
67707a54b5 fixes #18367 and #21222; using quote inside static block (#25907)
fixes #18367
fixes #21222


1. In vmdef.nim:304, newCtx now sets templInstCounter: new int when it
builds TCtx.
2. vm.nim:1490 — During VM execution of templates, c.templInstCounter is
passed to evalTemplate
3. evaltempl.nim:204 — instID: instID[] dereferences the ref int
4. With a nil templInstCounter, this would crash
5. The same initialization already exists on the semantic side in
sem.nim:787, so this change makes the VM path consistent with the rest
of the compiler.
2026-06-13 07:04:17 +02:00
ringabout
0f751695e4 fixes #25903 and #25904; add closure iterators with error handling (#25905)
fixes #25903
fixes #25904


`nkExceptBranch` can have variable structure depending on the exception
types and it should handle the last node of the `nkExceptBranch`
2026-06-13 07:03:28 +02:00
ringabout
9db9b8ce57 adds regression tests (#25906)
closes #22842, closes #21252, closes #19312, 
closes #16956, closes #16416, closes #14913, closes #13296,
closes #12424, closes #10902, closes #9892, closes #9617
2026-06-13 11:06:51 +08:00
WyattBlue
b44d373b7d adds wasm64 (Memory64) as a first-class target (#25900)
This pull request allows setting `--cpu:wasm64`, allowing wasm64 as a
first class target. This avoids having to set `-cpu:riscv64` as a
workaround. Sane defaults for the emscripten toolchain are also
provided.
2026-06-11 23:49:50 +02:00
Jacek Sieka
7fa006c4e5 fix invalid join (#25896)
can't join a thread that wasn't started (causes random crashes)
2026-06-11 20:24:48 +02:00
Jacek Sieka
1376052519 memalloc: fix forward declarations (#25895)
None of them have side effects / all are gcsafe
2026-06-11 16:13:24 +02:00
Jacek Sieka
13d152a4d1 fix state array constant types (#25893)
else there's a mismatch in the AST for the bracket constructor
2026-06-11 16:12:28 +02:00
Jacek Sieka
c620adcfce astyaml: formatting fixes (#25897)
fix missing indent and newlines here and there
2026-06-11 14:10:42 +02:00
Jacek Sieka
eaa4b342be system: remove unused exception raising code (#25894)
...that otherwise causes an unnecessary raise effect on writeWindows /
echoBinSafe
2026-06-11 10:33:51 +02:00
Ryan McConnell
0448557bfe fix 25778; concept coerces incompatible types (#25781)
I don't like it, but seems like this is correct. Concept type classes
have to behave like other "named" type classes and participate in "bind
once" mechanics or require some weird semantics. As a side note I'm
pretty sure the `tuple` example in the manual explaining this is either
wrong now or has regressed, but I don't think it matters because I doubt
anyone thinks about this feature much.
#25778
2026-06-11 08:17:35 +02:00
ringabout
07685f79e0 implements fallback memfiles on Nintendoswitch (#25891)
fix hightlies failures
2026-06-11 08:15:30 +02:00
ringabout
f5c43ad759 closes #25885; adds a test case (#25892)
closes #25885
2026-06-11 13:07:10 +08:00
Tomohiro
48621c217f adds modifierMode parameter to typeof (#25815)
This PR adds 3 modes to `typeof` to specify how to handle type modifiers
`var`, `sink` and `lent`.

- typeOfModCompatible
Remove or keep type modifiers in the same way as old typeof. That means
keep `sink` but remove `var` and `lent`.
- typeOfModRemoveModifier
  Remove type modifiers.
- typeOfModKeepModifier
  Keep type modifiers.

Related to https://github.com/nim-lang/Nim/pull/25779
https://github.com/nim-lang/Nim/issues/25786
2026-06-09 20:55:30 +02:00
ringabout
4d0663096c Revert "fixes #22122; raise effects for complex expressions" (#25888)
Reverts nim-lang/Nim#25845

```nim
case ecode
of ECONNABORTED, EPERM, ETIMEDOUT, ENOTCONN:
  getConnectionAbortedError(ecode)
of EMFILE, ENFILE, ENOBUFS, ENOMEM:
  getTransportTooManyError(ecode)
else:
  (ref TransportOsError)(code: ecode,
                         msg: "(" & $int(ecode) & ") " & osErrorMsg(ecode))
```

The compiler inserts a hidden conv for the case expression. Perhaps we
can skip hidden convs to inspect the types that are actually raised
2026-06-09 14:46:48 +02:00
ringabout
e942da94b5 fixes #22122; raise effects for complex expressions (#25845)
fixes #22122

The root cause is in the effect tracker: raise was recording the whole
conditional expression as one exception source, so semantic checking
only saw the widened common base type instead of the concrete exception
classes from each branch.
2026-06-08 22:59:16 +02:00
Aleksei Rybnikov
b6842c144d fix(uri): ? operator now appends to existing query string (#25831)
## Summary

Fixes #19782.

The `?` operator in `std/uri` was silently overwriting any query string
already present in the URI. This PR makes it append instead — which
matches the docstring ("Concatenates the query parameters") and the
natural expectation when chaining operations.

**Before:**
```nim
let u = parseUri("https://example.com/foo?existing=1") ? {"bar": "qux"}
echo $u  # https://example.com/foo?bar=qux  (existing=1 lost)
```

**After:**
```nim
let u = parseUri("https://example.com/foo?existing=1") ? {"bar": "qux"}
echo $u  # https://example.com/foo?existing=1&bar=qux
```

## Changes

- `lib/pure/uri.nim`: fix `?` to append with `&` when a query string
already exists; add example to `runnableExamples`
- `tests/stdlib/turi.nim`: two new test cases (append to existing query,
empty params preserve existing)
- `changelog.md`: entry under Standard library changes

## Notes

I work with Claude as a co-processor. I'm 56, came to programming late,
and this is genuinely how I learn and contribute. I understand what I'm
submitting, but I didn't write it alone. If your project prefers
human-only contributions, just say so and I'll close without friction.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: n0madgang <14005836+n0madgang@users.noreply.github.com>
2026-06-08 22:58:44 +02:00
ringabout
b000d4a32a uses lent for sets (#25882) 2026-06-08 22:57:33 +02:00
Andreas Rumpf
7a5e35c83e fixes #25693; continues the bugfix story (#25876) 2026-06-08 22:54:03 +02:00
ringabout
2d148edeb8 adds a test case for #25872 (#25880) 2026-06-08 22:47:42 +08:00
Andreas Rumpf
d9e28aac8e parser: concept of (#25878)
Co-authored-by: Gerke Max Preussner <gmpreussner@headcrash.industries>
2026-06-08 11:32:04 +02:00
Andreas Rumpf
c84764a097 emit modern NIF-27 (#25877) 2026-06-08 09:13:26 +02:00
ringabout
1d7510dff0 fixes #22936; Generic inheritance matching gives type mismatch when object has members (#25836)
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.
2026-06-08 09:12:00 +02:00
Tomohiro
9b80b2e868 fixes-25655; defining >= operator generates compile error (#25787)
Fixes https://github.com/nim-lang/Nim/issues/25655

---------

Co-authored-by: Andreas Rumpf <araq4k@proton.me>
2026-06-08 09:00:00 +02:00
ringabout
f5930d0bb3 fixes #20811; Nested proc with inner being generic cannot access parameters of outer proc (#25837)
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.
2026-06-08 08:55:37 +02:00
ringabout
4497d89267 fixes #18238; Nested object construction can zero same memory multiple times for --mm:refc (#25834)
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
2026-06-08 08:54:15 +02:00
ringabout
f959a02037 fixes #25725; environment misses: s with iterator (#25828)
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).
2026-06-08 08:53:10 +02:00
Andreas Rumpf
3c6449dbdd fixes #25850 (#25875) 2026-06-07 19:55:56 +02:00
ringabout
f1ff8b6d9e fixes #25849; fixes #25872; Iteration on elements of array (#25860)
fixes #25849
fixes https://github.com/nim-lang/Nim/issues/25872
2026-06-06 07:58:19 +02:00
Ryan McConnell
46259cd0b8 fix sortVTableDispatchers KeyError on re-entrant method registration via when isMainModule (#25856)
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.
2026-06-05 16:37:00 +02:00
ringabout
4b374eb0a6 stop a temp register from being freed if addressed for lent (#25861)
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.
2026-06-04 13:29:48 +02:00
Corey Leavitt
c8e805a2fa fixes #25595; cursor inference: a recorded mutation extends the variable's liveness (#25864)
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).
2026-06-03 07:25:33 +02:00
Corey Leavitt
73986c03a1 fixes #25857; don't treat typeof(result) as a use-before-init of result (#25858)
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.
2026-06-02 07:07:44 +02:00
ringabout
88a18de44f fixes #25851; ensure --panics:on does not skip nimErr_ check after closure calls (#25855)
fixes #25851

## Summary: `--panics:on` drops `nimErr_` check after closure calls
(#25851)

### Bug

With `--exceptions:goto` and `--panics:on`, the compiler skipped the
`nimErr_` check after indirect closure calls whose result flows directly
into another call (e.g., `result.add elem(src)`). A raise inside the
closure was silently swallowed — the loop continued, and the next
`raise` hit the already-set `nimInErrorMode` flag, overflowing its
`bool` storage into `OverflowDefect`.

### Root Cause

**ast.nim** — `canRaise` checked `fn.typ.n[0].len < effectListLen` first
(false after the expansion) and then `exceptionEffects != nil` (also
false, nil), so it returned `false` — meaning "cannot raise." The C
codegen trusted this and omitted the `nimErr_` check.

### Fix

**ast.nim** — `canRaise` now treats `nil` `exceptionEffects` as "unknown
→ can raise" (`exceptionEffects == nil` as an additional true
condition). This is defense-in-depth: even if some other path expands
the list but leaves `exceptionEffects` nil (e.g., a type with `{.tags.}`
but no `{.raises.}`), the error check is still emitted.

### Test

tclosure_err_panic_goto.nim — exercises the double-trigger pattern
(`drawBool` sets the error flag → closure call must propagate it) with
`matrix: "; --panics:on"` covering both exception modes.
2026-06-01 16:21:37 +02:00
Andreas Rumpf
7813bd8b92 fixes #25693 (#25842) 2026-05-29 08:08:42 +02:00
ringabout
645e131739 fixes #25796; fixes procParamTypeRel to ensure backend type consistency (#25798)
fixes #25796

This pull request addresses a subtle type-matching issue in the Nim
compiler related to backend type compatibility, particularly for
procedures returning `lent` types. It also adds new test cases to ensure
correct handling of these scenarios.

**Compiler type-checking fix:**

* Updated `procParamTypeRel` in `compiler/sigmatch.nim` to skip wrappers
like `tyVar`, `tyLent`, `tySink`, and `tyOwned` before comparing backend
types, ensuring more accurate type equivalence checks for procedure
parameters and return types.

**Test coverage improvements:**

* Added multiple blocks in `tests/proc/tproc.nim` to test procedure
types returning `lent` objects, including cases with constants,
variables, and union parameter types, verifying that the compiler now
correctly handles these cases.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-29 07:58:23 +02:00
puffball1567
7d2f28b046 fixes ReraiseDefect after typeless except: + finally: (cpp backend) (#25777)
## Bug

A bare `except:` followed by a `finally:` block raises a spurious
`ReraiseDefect: no exception to reraise` when compiled with `nim cpp`:

```nim
proc test() =
  try:
    raise newException(CatchableError, "x")
  except:
    discard
  finally:
    echo "finally"

test()
echo "after"
```

Expected output:
```
finally
after
```

Actual output:
```
finally
fatal.nim(53)            sysFatal
Error: unhandled exception: no exception to reraise [ReraiseDefect]
```

This reproduces on every memory manager (`--mm:arc`, `--mm:orc`,
`--mm:refc`).

## Root cause

`genTryCpp` emits `try { ... } catch (Exception* T_) { ... }` followed
by a finally block that ends with `if (T_) std::rethrow_exception(T_);`.
In the *typed* except branches the codegen explicitly sets `T_ =
nullptr;` once the exception is handled, so the rethrow check in the
finally is a no-op.

The typeless `except:` branch (the `if t[i].len == 1` arm) emitted only
`popCurrentException()` and forgot to clear `T_`. After the handler body
finished, `T_` still pointed at the original exception, so the trailing
`if (T_) std::rethrow_exception(T_);` rethrew it. By that point Nim's
current-exception stack had already been popped, and the rethrow
surfaced as `ReraiseDefect`.

## Fix

Emit `T_ = nullptr;` at the start of the typeless `except:` handler
body, mirroring what is already done for the typed branches. This is the
same one-line treatment that fixed the analogous typed-except case for
#5871.

## Tests

Adds `tests/exception/treraise_typeless_except_finally.nim`, exercising
the bug pattern on `--mm:arc`, `--mm:orc`, and `--mm:refc`.

Locally:
- `tests/exception/` — 43 PASS, 0 FAIL, 3 SKIP
- new test passes on all three memory managers

## Backport

Tagged `[backport]` in the commit message — the same bug exists in
`version-2-2` and the fix applies cleanly there.

## Related

Independent of, but in the same family as, #25775 (also currently open).
Both are silent-finally / cpp-backend exception handling fixes; they
touch different lines of `genTryCpp` and don't conflict.

Co-authored-by: puffball1567 <17452514+puffball1567@users.noreply.github.com>
2026-05-29 07:53:37 +02:00
Antonis Geralis
f4dd00c4cc Scan until next special char (", \, \0, \c, \L) and append that slice once. (#25498)
Benchmark comparison (-d:danger --mm:arc --debugger:native -d:useMalloc,
  OpenAI file benchmark, 5 runs):

- Before: 0.196674934, 0.189423191, 0.198763300, 0.197125584,
0.205015032
- After: 0.182827130, 0.183330852, 0.174878542, 0.174360811, 0.181704921
  - Median before: 0.197125584s
  - Median after: 0.181704921s
  - Improvement: 7.82% faster

  Callgrind comparison (same build flags):

  - Total Ir before: 3,219,477,120
  - Total Ir after: 2,449,556,167
  - Total Ir reduction: 23.91%

  parseString hotspot:

  - Before: 1,343,343,723 Ir
  - After: 573,423,735 Ir
  - Reduction: 57.31%
2026-05-27 23:31:39 +02:00
ringabout
3e2cea21ed fixes #22791; ProveField warning with nested case object (#25774)
fixes #22791

This pull request introduces a minor improvement to the handling of
immutable variables in the compiler and adds a new test case for nested
case objects. The most important changes are:

### Compiler improvements

* Updated the `isLet` guard in `compiler/guards.nim` to recognize
`skConst` symbols as immutable variables, ensuring that constants are
correctly identified alongside lets and other immutable types.

### Test coverage

* Added a new test in `tests/objvariant/tcorrectcheckedfield.nim` for
bug #22791, verifying correct pattern matching and field access in
nested `case` objects with constants.
2026-05-27 23:29:27 +02:00
ringabout
cfa769fefc fixes #22950; Poor error message on cast effect violation (#25839)
fixes #22950

This pull request improves the tracking and reporting of effect
annotations (such as `raises`, `tags`, and `forbids`) in pragma blocks,
particularly when using the `cast` pragma. It ensures that the source of
these effect annotations is correctly preserved and referenced, which
improves error reporting and effect analysis. Additionally, a new test
was added to check for violations when using `cast` with effect
annotations.

Effect annotation source tracking and propagation:

* Added new fields (`excSource`, `tagsSource`, `forbidsSource`) to the
`PragmaBlockContext` type to store the original source node for each
effect annotation.
* Updated `castBlock` to set these new source fields when processing
`raises`, `tags`, and `forbids` pragmas, ensuring the source node is
preserved for later error reporting.
* Modified `unapplyBlockContext` to use the stored source node (if
available) when calling `addRaiseEffect`, `addTag`, and `addNotTag`,
improving the accuracy of effect tracking and diagnostics.

Pragma handling improvements:

* Changed the call to `castBlock` in the main pragma processing loop to
pass the entire pragma node, enabling access to the original source for
effect annotations.

Testing:

* Added a new test (`tests/effects/tcast_effect_violation.nim`) to
verify that using `cast(raises: ValueError)` inside a procedure with
`.raises: [].` correctly triggers an error message about an unlisted
exception.
2026-05-27 23:28:27 +02:00
158 changed files with 8101 additions and 1518 deletions

View File

@@ -49,7 +49,7 @@ jobs:
DEBIAN_FRONTEND='noninteractive' \
sudo apt-get install --no-install-recommends -yq \
libcurl4-openssl-dev libgc-dev libsdl1.2-dev libsfml-dev \
valgrind libc6-dbg libblas-dev liblapack-dev libpcre2-dev xorg-dev
valgrind libc6-dbg libblas-dev liblapack-dev libpcre3 xorg-dev
- name: 'Install dependencies (macOS)'
if: runner.os == 'macOS'
run: brew install boehmgc make sfml gtk+3

View File

@@ -43,6 +43,15 @@ parameter and result types, not just their source-level shape. Use
[//]: # "Additions:"
- Added `system.readRawDataStable`, a companion to `readRawData` that returns a
raw `ptr UncheckedArray[char]` into a string's character data which stays valid
across moves and copies of the string value. It is available under every string
implementation (refc, ARC/ORC and `--strings:sso`) with the same signature, so
code can pin an interior buffer pointer today and be ready for `--strings:sso`
without `when declared` guards. Under `--strings:sso` it promotes a small inline
string to its heap representation first; under the other implementations the data
is already heap-resident, so it is equivalent to `readRawData`.
- `setutils.symmetricDifference` along with its operator version
`` setutils.`-+-` `` and in-place version `setutils.toggle` have been added
to more efficiently calculate the symmetric difference of bitsets.
@@ -72,18 +81,22 @@ parameter and result types, not just their source-level shape. Use
- `std/nre2` is added to replace deprecated NRE.
- `system.typeof` adds a new parameter `modifierMode` to specify how type modifiers are handled.
[//]: # "Changes:"
- `std/math` The `^` symbol now supports floating-point as exponent in addition to the Natural type.
- `min`, `max`, and `sequtils`' `minIndex`, `maxIndex` and `minmax` for `openArray`s now accept a comparison function.
- `system.substr` implementation now uses `copymem` (wrapped C `memcpy`) for copying data, if available at compilation.
- `system.newStringUninit` is now considered free of side-effects allowing it to be used with `--experimental:strictFuncs`.
- `std/re` and `std/nre` now use PCRE2. They remain deprecated;
use https://github.com/nitely/nim-regex or `std/nre2`.
- `std/re` and `std/nre` are deprecated as PCRE library is obsolete.
Use https://github.com/nitely/nim-regex or `std/nre2`.
See: https://github.com/nim-lang/Nim/issues/23668.
- `std/pegs` now correctly lexes UTF-8 bytes inside bare identifier-style
terminals, so case-insensitive matching of non-ASCII terms (e.g. ``\i café``)
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).
## Language changes

View File

@@ -36,6 +36,13 @@ proc setupProgram*(config: ConfigRef; cache: IdentCache) =
when not defined(nimKochBootstrap):
program = createDecodeContext(config, cache)
proc setIcMainModule*(fileIdx: FileIndex) =
## Tells the IC loader which module is being compiled fresh, so that
## re-exports of that module's symbols by dependencies are not loaded as
## duplicate stubs.
when not defined(nimKochBootstrap):
ast2nif.setMainModule(program, fileIdx)
template loadSym(s: PSym) =
## Loads a symbol from NIF file if it's in Partial state.
when not defined(nimKochBootstrap):
@@ -70,6 +77,16 @@ proc backendEnsureMutable*(t: PType) {.inline.} =
# ^ IC review this later
if t.state == Partial: loadType(t)
proc unsealForTransform*(t: PType) {.inline.} =
## The transformer/lambda lifting also run inside `nim m` when the VM
## compiles a LOADED routine (macro evaluation, `getImpl`). Their mutations
## are process-local — transformed bodies are never written back to a NIF —
## so downgrade the loaded type to mutable, mirroring the `cmdNifC` loader
## which loads everything `Complete` for exactly this reason (see
## `ast2nif.loadedState`).
if t.state == Partial: loadType(t)
if t.state == Sealed: t.state = Complete
proc owner*(s: PSym): PSym {.inline.} =
if s.state == Partial: loadSym(s)
result = s.ownerFieldImpl
@@ -221,7 +238,10 @@ proc position*(s: PSym): int {.inline.} =
result = s.positionImpl
proc `position=`*(s: PSym, val: int) {.inline.} =
assert s.state != Sealed
# No `Sealed` guard: the VM reuses `position` as a register slot while compiling
# a macro for execution (see `vmgen.genGenericParams`), which under IC may be a
# macro loaded from a NIF file. The macro is run, not code-generated, so this
# scratch mutation is harmless.
if s.state == Partial: loadSym(s)
s.positionImpl = val
@@ -445,9 +465,13 @@ var gconfig {.threadvar.}: Gconfig
proc setUseIc*(useIc: bool) = gconfig.useIc = useIc
proc comment*(n: PNode): string =
if nfHasComment in n.flags and not gconfig.useIc:
# IC doesn't track comments, see `packed_ast`, so this could fail
result = gconfig.comments[n.nodeId]
if nfHasComment in n.flags:
# NIF-based IC doesn't serialize comments, but the comment table is keyed by
# the node's address (`nodeId`), which is unique among live nodes; a loaded
# node that carries `nfHasComment` simply has no entry here (its comment was
# set in another process), so `getOrDefault` safely returns "" for it while
# in-process VM macro nodes (e.g. newCommentStmtNode) still round-trip.
result = gconfig.comments.getOrDefault(n.nodeId)
else:
result = ""
@@ -478,13 +502,6 @@ proc getPIdent*(a: PNode): PIdent {.inline.} =
of nkOpenSymChoice, nkClosedSymChoice, nkOpenSym: a.sons[0].sym.name
else: nil
const
moduleShift = when defined(cpu32): 20 else: 24
template toId*(a: ItemId): int =
let x = a
(x.module.int shl moduleShift) + x.item.int
template id*(a: PType | PSym): int = toId(a.itemId)
type
@@ -493,28 +510,44 @@ type
symId*: int32
typeId*: int32
sealed*: bool
backendMinted*: bool
disambTable*: CountTable[PIdent]
const
PackageModuleId* = -3'i32
proc idGeneratorFromModule*(m: PSym): IdGenerator =
assert m.kind == skModule
result = IdGenerator(module: m.itemId.module, symId: m.itemId.item, typeId: 0, disambTable: initCountTable[PIdent]())
result.disambTable.inc m.name
proc idGeneratorForBackend*(m: PSym): IdGenerator =
## Like `idGeneratorFromModule`, but for IC codegen (`nim nifc`): symbols and
## types minted fresh during codegen (transf labels/temps, lifted hooks, type
## copies) must not collide with the itemIds the NIF loader synthesizes for
## lazily-loaded symbols/types of the same module — those come from a
## per-module load-order counter that keeps running while codegen mints its
## own ids. A collision corrupts itemId-keyed tables, e.g. `transf`'s inline
## iterator mapping then substitutes a random loaded sym (a call's callee)
## with a `:tmp` block label. Backend-minted ids carry a marker bit in the
## module half (see `itemids.backendItemId`), so the two id spaces are
## disjoint by construction.
assert m.kind == skModule
result = IdGenerator(module: m.itemId.module, symId: 0, typeId: 0,
backendMinted: true, disambTable: initCountTable[PIdent]())
result.disambTable.inc m.name
proc idGeneratorForPackage*(nextIdWillBe: int32): IdGenerator =
result = IdGenerator(module: PackageModuleId, symId: nextIdWillBe - 1'i32, typeId: 0, disambTable: initCountTable[PIdent]())
proc nextSymId(x: IdGenerator): ItemId {.inline.} =
assert(not x.sealed)
inc x.symId
result = ItemId(module: x.module, item: x.symId)
result = if x.backendMinted: backendItemId(x.module, x.symId)
else: itemId(x.module, x.symId)
proc nextTypeId*(x: IdGenerator): ItemId {.inline.} =
assert(not x.sealed)
inc x.typeId
result = ItemId(module: x.module, item: x.typeId)
result = if x.backendMinted: backendItemId(x.module, x.typeId)
else: itemId(x.module, x.typeId)
when false:
proc nextId*(x: IdGenerator): ItemId {.inline.} =
@@ -1043,6 +1076,11 @@ proc newType*(kind: TTypeKind; idgen: IdGenerator; owner: PSym; son: sink PType
if result.itemId.module == 55 and result.itemId.item == 2:
echo "KNID ", kind
writeStackTrace()
when defined(icDbg):
if kind == tyOpenArray:
echo "NEWTYPE openArray id=", id.module, ".", id.item,
" owner=", (if owner != nil: owner.name.s else: "nil")
echo getStackTrace()
proc setSons*(dest: PType; sons: sink seq[PType]) {.inline.} =
assert dest.kind != tyProc or sons.len <= 1
@@ -1105,10 +1143,19 @@ proc copyType*(t: PType, idgen: IdGenerator, owner: PSym): PType =
assignType(result, t)
result.symImpl = t.sym # backend-info should not be copied
proc exactReplica*(t: PType): PType =
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,
## 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: t.uniqueId)
uniqueId: nextTypeId(idgen))
assignType(result, t)
result.symImpl = t.sym # backend-info should not be copied
@@ -1271,6 +1318,9 @@ proc transitionNoneToSym*(n: PNode) =
transitionNodeKindCommon(nkSym)
template transitionSymKindCommon*(k: TSymKind) =
# Under IC the symbol may still be an unloaded stub (`skStub`); materialise it
# first so its kind-specific fields (read below as `obj.*`) actually exist.
if s.state == Partial: loadSym(s)
let obj {.inject.} = s[]
s[] = TSym(kindImpl: k, itemId: obj.itemId, magicImpl: obj.magicImpl, typImpl: obj.typImpl, name: obj.name,
infoImpl: obj.infoImpl, ownerFieldImpl: obj.ownerFieldImpl, flagsImpl: obj.flagsImpl, astImpl: obj.astImpl,
@@ -1647,9 +1697,13 @@ proc canRaise*(fn: PNode): bool =
if fn.typ.n[0].kind == nkSym:
result = 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.
result = ((fn.typ.n[0].len < effectListLen) or
(fn.typ.n[0][exceptionEffects] != nil and
fn.typ.n[0][exceptionEffects].safeLen > 0))
fn.typ.n[0][exceptionEffects] == nil or
fn.typ.n[0][exceptionEffects].safeLen > 0)
else:
result = false

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,9 @@ export int128
import nodekinds
export nodekinds
import itemids
export itemids
type
TCallingConvention* = enum
ccNimCall = "nimcall" # nimcall, also the default
@@ -571,23 +574,6 @@ const
generatedMagics* = {mNone, mIsolate, mFinished, mOpenArrayToSeq}
## magics that are generated as normal procs in the backend
type
ItemId* = object
module*: int32
item*: int32
proc `$`*(x: ItemId): string =
"(module: " & $x.module & ", item: " & $x.item & ")"
proc `==`*(a, b: ItemId): bool {.inline.} =
a.item == b.item and a.module == b.module
proc hash*(x: ItemId): Hash =
var h: Hash = hash(x.module)
h = h !& hash(x.item)
result = !$h
type
PNode* = ref TNode
TNodeSeq* = seq[PNode]
@@ -1000,7 +986,8 @@ proc newStrNode*(strVal: string; info: TLineInfo): PNode =
type
LogEntryKind* = enum
HookEntry, ConverterEntry, MethodEntry, EnumToStrEntry, GenericInstEntry
HookEntry, ConverterEntry, MethodEntry, EnumToStrEntry, GenericInstEntry,
PureEnumEntry
LogEntry* = object
kind*: LogEntryKind
op*: TTypeAttachedOp

View File

@@ -43,13 +43,13 @@ proc flagsToStr[T](flags: set[T]): string =
proc lineInfoToStr*(conf: ConfigRef; info: TLineInfo): string =
result = "["
result.addYamlString(toFilename(conf, info))
result.addf ", $1, $2]", [toLinenumber(info), toColumn(info)]
result.addf ", $1, $2]", toLinenumber(info), toColumn(info)
proc treeToYamlAux(res: var string; conf: ConfigRef; n: PNode; marker: var IntSet; indent, maxRecDepth: int)
proc symToYamlAux(res: var string; conf: ConfigRef; n: PSym; marker: var IntSet; indent, maxRecDepth: int)
proc typeToYamlAux(res: var string; conf: ConfigRef; n: PType; marker: var IntSet; indent, maxRecDepth: int)
proc treeToYamlAux(res: var string; conf: ConfigRef; n: PNode; marker: var IntSet; nl: bool, indent, maxRecDepth: int)
proc symToYamlAux(res: var string; conf: ConfigRef; n: PSym; marker: var IntSet; nl: bool, indent, maxRecDepth: int)
proc typeToYamlAux(res: var string; conf: ConfigRef; n: PType; marker: var IntSet; nl: bool, indent, maxRecDepth: int)
proc symToYamlAux(res: var string; conf: ConfigRef; n: PSym; marker: var IntSet; indent: int; maxRecDepth: int) =
proc symToYamlAux(res: var string; conf: ConfigRef; n: PSym; marker: var IntSet; nl: bool, indent: int; maxRecDepth: int) =
if n == nil:
res.add("null")
elif containsOrIncl(marker, n.id):
@@ -57,10 +57,12 @@ proc symToYamlAux(res: var string; conf: ConfigRef; n: PSym; marker: var IntSet;
else:
let istr = spaces(indent * 4)
if nl:
res.addf("\n$1", istr)
res.addf("kind: $1", [makeYamlString($n.kind)])
res.addf("\n$1name: $2", [istr, makeYamlString(n.name.s)])
res.addf("\n$1typ: ", [istr])
res.typeToYamlAux(conf, n.typ, marker, indent + 1, maxRecDepth - 1)
res.typeToYamlAux(conf, n.typ, marker, true, indent + 1, maxRecDepth - 1)
if conf != nil:
# if we don't pass the config, we probably don't care about the line info
res.addf("\n$1info: $2", [istr, lineInfoToStr(conf, n.info)])
@@ -68,7 +70,7 @@ proc symToYamlAux(res: var string; conf: ConfigRef; n: PSym; marker: var IntSet;
res.addf("\n$1flags: $2", [istr, flagsToStr(n.flags)])
res.addf("\n$1magic: $2", [istr, makeYamlString($n.magic)])
res.addf("\n$1ast: ", [istr])
res.treeToYamlAux(conf, n.ast, marker, indent + 1, maxRecDepth - 1)
res.treeToYamlAux(conf, n.ast, marker, true, indent + 1, maxRecDepth - 1)
res.addf("\n$1options: $2", [istr, flagsToStr(n.options)])
res.addf("\n$1position: $2", [istr, $n.position])
res.addf("\n$1k: $2", [istr, makeYamlString($n.loc.k)])
@@ -76,53 +78,57 @@ proc symToYamlAux(res: var string; conf: ConfigRef; n: PSym; marker: var IntSet;
if card(n.loc.flags) > 0:
res.addf("\n$1flags: $2", [istr, makeYamlString($n.loc.flags)])
res.addf("\n$1snippet: $2", [istr, n.loc.snippet])
res.addf("\n$1lode: $2", [istr])
res.treeToYamlAux(conf, n.loc.lode, marker, indent + 1, maxRecDepth - 1)
res.addf("\n$1lode: ", [istr])
res.treeToYamlAux(conf, n.loc.lode, marker, true, indent + 1, maxRecDepth - 1)
proc typeToYamlAux(res: var string; conf: ConfigRef; n: PType; marker: var IntSet; indent: int; maxRecDepth: int) =
proc typeToYamlAux(res: var string; conf: ConfigRef; n: PType; marker: var IntSet; nl: bool, indent: int; maxRecDepth: int) =
if n == nil:
res.add("null")
elif containsOrIncl(marker, n.id):
res.addf "\"$1 @$2\"" % [$n.kind, strutils.toHex(cast[uint](n), sizeof(n) * 2)]
else:
let istr = spaces(indent * 4)
if nl:
res.addf("\n$1", istr)
res.addf("kind: $2", [istr, makeYamlString($n.kind)])
res.addf("\n$1sym: ")
res.symToYamlAux(conf, n.sym, marker, indent + 1, maxRecDepth - 1)
res.addf("\n$1n: ")
res.treeToYamlAux(conf, n.n, marker, indent + 1, maxRecDepth - 1)
res.addf("\n$1sym: ", istr)
res.symToYamlAux(conf, n.sym, marker, true, indent + 1, maxRecDepth - 1)
res.addf("\n$1n: ", istr)
res.treeToYamlAux(conf, n.n, marker, true, indent + 1, maxRecDepth - 1)
if card(n.flags) > 0:
res.addf("\n$1flags: $2", [istr, flagsToStr(n.flags)])
res.addf("\n$1callconv: $2", [istr, makeYamlString($n.callConv)])
res.addf("\n$1size: $2", [istr, $(n.size)])
res.addf("\n$1align: $2", [istr, $(n.align)])
if n.hasElementType:
res.addf("\n$1sons:")
res.addf("\n$1sons:", istr)
for a in n.kids:
res.addf("\n - ")
res.typeToYamlAux(conf, a, marker, indent + 1, maxRecDepth - 1)
res.addf("\n$1 - ", istr)
res.typeToYamlAux(conf, a, marker, false, indent + 1, maxRecDepth - 1)
proc treeToYamlAux(res: var string; conf: ConfigRef; n: PNode; marker: var IntSet; indent: int;
proc treeToYamlAux(res: var string; conf: ConfigRef; n: PNode; marker: var IntSet; nl: bool, indent: int;
maxRecDepth: int) =
if n == nil:
res.add("null")
else:
var istr = spaces(indent * 4)
if nl:
res.addf("\n$1", istr)
res.addf("kind: $1" % [makeYamlString($n.kind)])
if maxRecDepth != 0:
if conf != nil:
res.addf("\n$1info: $2", [istr, lineInfoToStr(conf, n.info)])
case n.kind
of nkCharLit .. nkInt64Lit:
of nkCharLit .. nkUInt64Lit:
res.addf("\n$1intVal: $2", [istr, $(n.intVal)])
of nkFloatLit, nkFloat32Lit, nkFloat64Lit:
of nkFloatLit .. nkFloat128Lit:
res.addf("\n$1floatVal: $2", [istr, n.floatVal.toStrMaxPrecision])
of nkStrLit .. nkTripleStrLit:
res.addf("\n$1strVal: $2", [istr, makeYamlString(n.strVal)])
of nkSym:
res.addf("\n$1sym: ", [istr])
res.symToYamlAux(conf, n.sym, marker, indent + 1, maxRecDepth)
res.symToYamlAux(conf, n.sym, marker, true, indent + 1, maxRecDepth)
of nkIdent:
if n.ident != nil:
res.addf("\n$1ident: $2", [istr, makeYamlString(n.ident.s)])
@@ -133,22 +139,22 @@ proc treeToYamlAux(res: var string; conf: ConfigRef; n: PNode; marker: var IntSe
res.addf("\n$1sons: ", [istr])
for i in 0 ..< n.len:
res.addf("\n$1 - ", [istr])
res.treeToYamlAux(conf, n[i], marker, indent + 1, maxRecDepth - 1)
res.treeToYamlAux(conf, n[i], marker, false, indent + 1, maxRecDepth - 1)
if n.typ != nil:
res.addf("\n$1typ: ", [istr])
res.typeToYamlAux(conf, n.typ, marker, indent + 1, maxRecDepth)
res.typeToYamlAux(conf, n.typ, marker, true, indent + 1, maxRecDepth)
proc treeToYaml*(conf: ConfigRef; n: PNode; indent: int = 0; maxRecDepth: int = -1): string =
var marker = initIntSet()
result = newStringOfCap(1024)
result.treeToYamlAux(conf, n, marker, indent, maxRecDepth)
result.treeToYamlAux(conf, n, marker, false, indent, maxRecDepth)
proc typeToYaml*(conf: ConfigRef; n: PType; indent: int = 0; maxRecDepth: int = -1): string =
var marker = initIntSet()
result = newStringOfCap(1024)
result.typeToYamlAux(conf, n, marker, indent, maxRecDepth)
result.typeToYamlAux(conf, n, marker, false, indent, maxRecDepth)
proc symToYaml*(conf: ConfigRef; n: PSym; indent: int = 0; maxRecDepth: int = -1): string =
var marker = initIntSet()
result = newStringOfCap(1024)
result.symToYamlAux(conf, n, marker, indent, maxRecDepth)
result.symToYamlAux(conf, n, marker, false, indent, maxRecDepth)

View File

@@ -394,7 +394,7 @@ proc genArg(p: BProc, n: PNode, param: PSym; call: PNode; result: var Builder; n
# variable. Thus, we create a temporary pointer variable instead.
let needsIndirect = mapType(p.config, n[0].typ, mapTypeChooser(n[0]) == skParam) != ctArray
if needsIndirect:
n.typ = n.typ.exactReplica
n.typ = n.typ.exactReplica(p.module.idgen)
n.typ.incl tfVarIsPtr
a = initLocExprSingleUse(p, n)
a = withTmpIfNeeded(p, a, needsTmp)
@@ -909,6 +909,16 @@ proc isInactiveDestructorCall(p: BProc, e: PNode): bool =
proc genAsgnCall(p: BProc, le, ri: PNode, d: var TLoc) =
if p.withinBlockLeaveActions > 0 and isInactiveDestructorCall(p, ri):
return
when defined(icDbgHash):
if ri[0].typ == nil:
echo "NILCALLEE kind=", ri[0].kind,
" sym=", (if ri[0].kind == nkSym: ri[0].sym.name.s else: "-"),
" symKind=", (if ri[0].kind == nkSym: $ri[0].sym.kind else: "-"),
" flags=", (if ri[0].kind == nkSym: $ri[0].sym.flags else: "-"),
" lazy=", nfLazyType in ri[0].flags,
" inProc=", (if p.prc != nil: p.prc.name.s else: "NIL"),
" module=", p.module.module.name.s
raiseAssert "nil callee type, see NILCALLEE above"
if ri[0].typ.skipTypes({tyGenericInst, tyAlias, tySink, tyOwned}).callConv == ccClosure:
genClosureCall(p, le, ri, d)
elif ri[0].kind == nkSym and sfInfixCall in ri[0].sym.flags:

View File

@@ -1904,7 +1904,9 @@ proc genObjConstr(p: BProc, e: PNode, d: var TLoc) =
var tmp: TLoc = default(TLoc)
var r: Rope
let needsZeroMem = p.config.selectedGC notin {gcArc, gcAtomicArc, gcOrc, gcYrc} or nfAllFieldsSet notin e.flags
let needsZeroMem =
nfAllFieldsSet notin e.flags or
(optSeqDestructors notin p.config.globalOptions and containsGarbageCollectedRef(t))
if useTemp:
tmp = getTemp(p, t)
r = rdLoc(tmp)
@@ -3489,7 +3491,23 @@ proc genConstDefinition(q: BModule; p: BProc; sym: PSym) =
data.addDeclWithVisibility(Private):
data.addVarWithInitializer(Local, actualConstName, typ = td):
genBracedInit(q.initProc, sym.astdef, isConst = true, sym.typ, data)
q.s[cfsData].add(extract(data))
if q.config.cmd == cmdNifC:
# Each `cg` process that demands this const emits its definition
# (emit-everywhere). Always declare it first (the data analogue of a proc
# prototype) so a TU whose copy the merge stage drops still has a valid
# declaration; wrap the definition as a droppable `'d'` unit the merge
# stage assigns to a single owner.
let cname = stripCnifMarks(actualConstName)
var decl = newBuilder("")
decl.addDeclWithVisibility(Extern):
decl.addVar(kind = Local, name = actualConstName, typ = td)
q.s[cfsData].add(extract(decl))
q.s[cfsData].add(cnifDefDirective(cname, "d", icNifName(q, sym)))
q.s[cfsData].add(extract(data))
q.s[cfsData].add(cnifEndDefs())
q.icDataDefs.add (cname, icNifName(q, sym))
else:
q.s[cfsData].add(extract(data))
if q.hcrOn:
# generate the global pointer with the real name
q.s[cfsVars].addVar(kind = Global, name = sym.loc.snippet,
@@ -3553,6 +3571,17 @@ proc expr(p: BProc, n: PNode, d: var TLoc) =
of skProc, skConverter, skIterator, skFunc:
#if sym.kind == skIterator:
# echo renderTree(sym.getBody, {renderIds})
if p.config.cmd == cmdNifC and
(isGenericRoutineStrict(sym) or sfCompileTime in sym.flags or
(sym.kind == skIterator and sym.typ.callConv == ccInline)):
# Under IC a module's top-level routine definitions are serialized as bare
# symbol references that reappear in the loaded statement list. Uninstantiated
# generic routines (incl. those with type-class params like `tuple`) and
# `.compileTime` routines have no run-time code, so skip them here.
# Inline iterators likewise have no standalone code — they are always inlined
# at their for-loop call sites by the transformer (only closure iterators get
# a standalone C function), so a bare serialized def reference is a no-op.
return
if sfCompileTime in sym.flags:
localError(p.config, n.info, "request to generate code for .compileTime proc: " &
sym.name.s)
@@ -3627,6 +3656,11 @@ proc expr(p: BProc, n: PNode, d: var TLoc) =
# echo renderTree(p.prc.ast, {renderIds})
internalError(p.config, n.info, "expr: param not init " & sym.name.s & "_" & $sym.id)
putLocIntoDest(p, d, sym.loc)
of skTemplate, skMacro:
# Under IC a module's top-level template/macro definitions are serialized as
# bare symbol references (only their interface matters), so they reappear in
# the loaded statement list. They are compile-time only and produce no code.
discard
else: internalError(p.config, n.info, "expr(" & $sym.kind & "); unknown symbol")
of nkNilLit:
if not isEmptyType(n.typ):

View File

@@ -1237,6 +1237,7 @@ proc genTryCpp(p: BProc, t: PNode, d: var TLoc) =
else:
scope = initScope(p.s(cpsStmts))
# we handled the error:
linefmt(p, cpsStmts, "T$1_ = nullptr;$n", [etmp])
expr(p, t[i][0], d)
linefmt(p, cpsStmts, "#popCurrentException();$n", [])
endBlockWith(p):
@@ -1985,4 +1986,9 @@ proc genStmts(p: BProc, t: PNode) =
if isPush: pushInfoContext(p.config, t.info)
expr(p, t, a)
if isPush: popInfoContext(p.config)
internalAssert p.config, a.k in {locNone, locTemp, locLocalVar, locExpr}
# A bare `nkSym` statement is how IC serializes a definition that lives inside a
# top-level block (e.g. a nested `proc`/`var`): codegen emits the definition and
# leaves the symbol's own location in `a` (e.g. `locProc`), which is discarded
# here, so the value-sanity check below does not apply to it.
internalAssert p.config, t.kind == nkSym or
a.k in {locNone, locTemp, locLocalVar, locExpr}

View File

@@ -72,6 +72,37 @@ proc mangleProc(m: BModule; s: PSym; makeUnique: bool): string =
else:
m.g.mangledPrcs.incl(result)
proc sharedInstanceCName(m: BModule; s: PSym): string =
## The module-free canonical C name for a content-keyed generic instance,
## or "" when the symbol must keep its module-suffixed name. With a shared
## name, every TU that instantiated the same generic with the same type
## arguments calls one extern definition (first claimant's TU embeds it,
## see `genProcLvl3`) instead of compiling its own static copy.
##
## The name is program-unique only if the 30-bit content hash does not
## collide for same-named instances of *different* instantiations across
## modules — the per-module probe in `setInstanceDisamb` cannot see that.
## Claimants therefore must present the same signature; on mismatch the
## later one keeps its module-suffixed name (no merge, still correct).
## Residual risk: same name and signature, different generic args, AND a
## 30-bit collision — vanishingly unlikely; a full-typeKey verification
## channel can close it later.
result = ""
if m.config.cmd == cmdNifC and s.kind in routineKinds and
(s.disamb and InstanceDisambBit) != 0'i32 and
s.typ != nil and s.typ.callConv != ccInline and not m.hcrOn and
{sfImportc, sfExportc, sfCodegenDecl} * s.flags == {}:
# The content-derived `disamb` is unique per process (collision-probed in
# `setInstanceDisamb`), so the mint-site-independent `_i<disamb>` name is
# safe to use directly; identical instances across modules collide on it
# exactly and the merge stage keeps one.
result = s.name.s.mangle & "_i" & $s.disamb
proc isSharedInstanceCName(m: BModule; s: PSym): bool =
m.config.cmd == cmdNifC and s.kind in routineKinds and
(s.disamb and InstanceDisambBit) != 0'i32 and
stripCnifMarks(s.loc.snippet) == s.name.s.mangle & "_i" & $s.disamb
proc fillBackendName(m: BModule; s: PSym) =
if s.loc.snippet == "":
var result: Rope
@@ -79,13 +110,22 @@ proc fillBackendName(m: BModule; s: PSym) =
m.g.config.symbolFiles == disabledSf:
result = mangleProc(m, s, false).rope
else:
result = s.name.s.mangle.rope
result.add mangleProcNameExt(m.g.graph, s)
let shared = sharedInstanceCName(m, s)
if shared.len > 0:
result = shared.rope
else:
result = s.name.s.mangle.rope
result.add mangleProcNameExt(m.g.graph, s)
if m.hcrOn:
result.add '_'
result.add(idOrSig(s, m.module.name.s.mangle, m.sigConflicts, m.config))
backendEnsureMutable s
s.locImpl.snippet = result
if m.config.cmd == cmdNifC:
# mark the name so the cnif artifact writer can turn every occurrence
# into a Symbol token; stripped from the actual C output in genModule
s.locImpl.snippet = markCName(result)
else:
s.locImpl.snippet = result
proc fillParamName(m: BModule; s: PSym) =
if s.loc.snippet == "":
@@ -373,6 +413,12 @@ proc getSimpleTypeDesc(m: BModule; typ: PType): Rope =
m.typeCache[sig] = result
proc pushType(m: BModule; typ: PType) =
when defined(icDbgRefc):
if typ.kind == tySequence and
typ.elementType.skipTypes({tyGenericInst, tyAlias, tySink}).kind == tyGenericParam:
echo "[icRefc] pushType seq-of-genericparam t=", typeToString(typ),
" itemId=", typ.itemId.module, ".", typ.itemId.item, " mod=", m.module.name.s
echo getStackTrace()
for i in 0..high(m.typeStack):
# pointer equality is good enough here:
if m.typeStack[i] == typ: return
@@ -618,6 +664,18 @@ proc genProcParams(m: BModule; t: PType, rettype: var Rope, params: var Builder,
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
# 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
# build `:envP` only lives in the routine's AST params, never in the proc
# *type's* `n`, so it never reaches here. Under IC `closureParams` re-shares
# the AST param node with `typ.n`, so the lifted `:envP` leaks into `t.n`;
# emitting it would produce a bogus extra parameter that collides with the
# `closureSetup` local (the "redeclared as different kind of symbol" / env
# pointer-type mismatch). We still must fill its name/loc (later passes such
# as `assignParam` and `closureSetup` reference it), but it is omitted from
# the C signature to match the from-source ABI.
let isClosureEnv = t.callConv == ccClosure and param.name.s == ":envP"
var descKind = dkParam
if m.config.backend == backendCpp and optByRef in param.options:
if param.typ.kind == tyGenericInst:
@@ -629,6 +687,7 @@ proc genProcParams(m: BModule; t: PType, rettype: var Rope, params: var Builder,
fillParamName(m, param)
fillLoc(param.locImpl, locParam, t.n[i],
param.paramStorageLoc)
if isClosureEnv: continue # name/loc filled, but not part of the C signature
var typ: Rope
if ccgIntroducedPtr(m.config, param, t.returnType) and descKind == dkParam:
typ = ptrType(getTypeDescWeak(m, param.typ, check, descKind))
@@ -1108,6 +1167,11 @@ proc getTypeDescAux(m: BModule; origTyp: PType, check: var IntSet; kind: TypeDes
tyUserTypeClass, tyUserTypeClassInst, tyInferred:
result = getTypeDescAux(m, skipModifier(t), check, kind)
else:
when defined(icDbgRefc):
echo "[icRefc] getTypeDescAux ", t.kind, " t=", typeToString(t),
" origTyp=", typeToString(origTyp), " t.itemId=", t.itemId.module, ".", t.itemId.item,
" sym=", (if t.sym != nil: t.sym.name.s else: "nil"),
" owner=", (if t.owner != nil: t.owner.name.s else: "nil")
internalError(m.config, "getTypeDescAux(" & $t.kind & ')')
result = ""
# fixes bug #145:
@@ -1146,6 +1210,10 @@ proc finishTypeDescriptions(m: BModule) =
var check = initIntSet()
while i < m.typeStack.len:
let t = m.typeStack[i]
when defined(icDbgRefc):
echo "[icRefc] finishTypeDescriptions[", i, "] mod=", m.module.name.s,
" t=", typeToString(t), " kind=", t.kind,
" itemId=", t.itemId.module, ".", t.itemId.item
if optSeqDestructors in m.config.globalOptions and t.skipTypes(abstractInst).kind == tySequence:
seqV2ContentType(m, t, check)
else:
@@ -1260,7 +1328,9 @@ proc genProcHeader(m: BModule; prc: PSym; result: var Builder; visibility: var D
elif prc.typ.callConv == ccInline or isNonReloadable(m, prc):
visibility = StaticProc
elif sfImportc notin prc.flags:
visibility = Private
if not isSharedInstanceCName(m, prc):
visibility = Private
# else: plain extern — the definition is shared across TUs
if asPtr:
result.addProcVar(m, prc, name, params, rettype, isStatic = isStaticVar, ignoreAttributes = true)
else:
@@ -1340,6 +1410,8 @@ proc genTypeInfoAuxBase(m: BModule; typ, origType: PType;
else:
m.s[cfsStrData].addDeclWithVisibility(Private):
m.s[cfsStrData].addVar(kind = Local, name = name, typ = "TNimType")
if m.config.cmd == cmdNifC:
m.icDataDefs.add (name, icNifName(m, origType))
proc genTypeInfoAux(m: BModule; typ, origType: PType, name: Rope;
info: TLineInfo) =
@@ -1627,8 +1699,13 @@ proc declareNimType(m: BModule; name: string; str: Rope, module: int) =
m.s[cfsTypeInit1].addArgument(hcrGlobal):
m.s[cfsTypeInit1].add("\"" & str & "\"")
else:
# cnif-mark the name: this extern declaration is the reference the
# def-retention check consults when the defining TU regenerates and
# the typeinfo cannot be re-demanded (type vanished) — the referencing
# TU must lose its reuse then instead of producing a link error
let declName = if m.config.cmd == cmdNifC: markCName(str) else: str
m.s[cfsStrData].addDeclWithVisibility(Extern):
m.s[cfsStrData].addVar(kind = Local, name = str, typ = nr)
m.s[cfsStrData].addVar(kind = Local, name = declName, typ = nr)
proc genTypeInfo2Name(m: BModule; t: PType): Rope =
var it = t
@@ -1767,6 +1844,8 @@ proc genTypeInfoV2OldImpl(m: BModule; t, origType: PType, name: Rope; info: TLin
cgsym(m, "TNimTypeV2")
m.s[cfsStrData].addDeclWithVisibility(Private):
m.s[cfsStrData].addVar(kind = Local, name = name, typ = "TNimTypeV2")
if m.config.cmd == cmdNifC:
m.icDataDefs.add (name, icNifName(m, origType))
var flags = 0
if not canFormAcycle(m.g.graph, t): flags = flags or 1
@@ -1829,8 +1908,15 @@ proc genTypeInfoV2OldImpl(m: BModule; t, origType: PType, name: Rope; info: TLin
proc genTypeInfoV2Impl(m: BModule; t, origType: PType, name: Rope; info: TLineInfo) =
cgsym(m, "TNimTypeV2")
m.s[cfsStrData].addDeclWithVisibility(Private):
# Under `nim nifc` every `cg` process that demands this type's RTTI emits its
# definition (emit-everywhere). The forward declaration must therefore be a
# real `extern` (not a tentative definition) so a TU whose copy the merge
# stage drops still only *declares* it; the definition itself is wrapped as a
# droppable `'d'` unit below and assigned to a single owner.
m.s[cfsStrData].addDeclWithVisibility(if m.config.cmd == cmdNifC: Extern else: Private):
m.s[cfsStrData].addVar(kind = Local, name = name, typ = "TNimTypeV2")
if m.config.cmd == cmdNifC:
m.icDataDefs.add (name, icNifName(m, origType))
var flags = 0
if not canFormAcycle(m.g.graph, t): flags = flags or 1
@@ -1891,7 +1977,12 @@ proc genTypeInfoV2Impl(m: BModule; t, origType: PType, name: Rope; info: TLineIn
else:
typeEntry.addField(typeInit, name = "flags"):
typeEntry.addIntValue(flags)
m.s[cfsVars].add extract(typeEntry)
if m.config.cmd == cmdNifC:
m.s[cfsVars].add(cnifDefDirective(name, "d", icNifName(m, origType)))
m.s[cfsVars].add extract(typeEntry)
m.s[cfsVars].add(cnifEndDefs())
else:
m.s[cfsVars].add extract(typeEntry)
if t.kind == tyObject and t.baseClass != nil and optEnableDeepCopy in m.config.globalOptions:
discard genTypeInfoV1(m, t, info)
@@ -1930,7 +2021,13 @@ proc genTypeInfoV2(m: BModule; t: PType; info: TLineInfo): Rope =
m.typeInfoMarkerV2[sig] = result
let owner = t.skipTypes(typedescPtrs).itemId.module
if owner != m.module.position and myModuleOpenForCodegen(m, FileIndex owner):
# 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
# definition into the owner module's *unwritten* backend module (discarded in
# this process) and emit only an extern here, leaving the symbol undefined.
let perModuleCg = m.config.cmd == cmdNifC and m.config.icBackendStage == "cg"
if not perModuleCg and owner != m.module.position and myModuleOpenForCodegen(m, FileIndex owner):
# make sure the type info is created in the owner module
discard genTypeInfoV2(m.g.mods[owner], origType, info)
# reference the type info as extern here
@@ -1997,6 +2094,10 @@ proc genTypeInfoV1(m: BModule; t: PType; info: TLineInfo): Rope =
let marker = m.g.typeInfoMarker.getOrDefault(sig)
if marker.str != "":
when defined(icDbgRefc):
if "catchableerror" in marker.str:
echo "[icNti] ", marker.str, " in mod=", m.module.name.s,
" -> extern:globalMarker owner=", marker.owner
cgsym(m, "TNimType")
cgsym(m, "TNimNode")
declareNimType(m, "TNimType", marker.str, marker.owner)
@@ -2007,8 +2108,16 @@ proc genTypeInfoV1(m: BModule; t: PType; info: TLineInfo): Rope =
result = "NTI$1$2_" % [rope(typeToC(t)), rope($sig)]
m.typeInfoMarker[sig] = result
when defined(icDbgRefc):
template dbgNti(branch: string) =
if "catchableerror" in result:
echo "[icNti] ", result, " in mod=", m.module.name.s, " -> ", branch
else:
template dbgNti(branch: string) = discard
let old = m.g.graph.emittedTypeInfo.getOrDefault($result)
if old != FileIndex(0):
dbgNti "extern:emittedTypeInfo"
cgsym(m, "TNimType")
cgsym(m, "TNimNode")
declareNimType(m, "TNimType", result, old.int)
@@ -2016,6 +2125,7 @@ proc genTypeInfoV1(m: BModule; t: PType; info: TLineInfo): Rope =
var owner = t.skipTypes(typedescPtrs).itemId.module
if owner != m.module.position and myModuleOpenForCodegen(m, FileIndex owner):
dbgNti "extern:ownerRouted"
# make sure the type info is created in the owner module
discard genTypeInfoV1(m.g.mods[owner], origType, info)
# reference the type info as extern here
@@ -2026,6 +2136,7 @@ proc genTypeInfoV1(m: BModule; t: PType; info: TLineInfo): Rope =
else:
owner = m.module.position.int32
dbgNti "DEFINED-HERE"
m.g.typeInfoMarker[sig] = (str: result, owner: owner)
#rememberEmittedTypeInfo(m.g.graph, FileIndex(owner), $result)

View File

@@ -112,10 +112,13 @@ 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
result.add(if s.itemId.isBackendMinted: "_c" else: "_u")
result.add $s.itemId.item
# module suffix LAST (a strippable trailing token; see `mangleProcNameExt`)
result.add "__"
result.add m.g.graph.ifaces[s.itemId.module].uniqueName
result.add "_u"
result.add $s.itemId.item
proc encodeSym*(m: BModule; s: PSym; makeUnique: bool = false; extra: string = ""): string =
#Module::Type

View File

@@ -19,6 +19,10 @@ import
mangleutils, cbuilderbase, modulegraphs
from expanddefaults import caseObjDefaultBranch
from ast2nif import globalName, toNifFilename, icNifTypeName
from typekeys import modname
from std/algorithm import sort
import cnif
import pipelineutils
@@ -51,7 +55,7 @@ when not declared(dynlib.libCandidates):
else:
dest.add(s)
when options.hasTinyCBackend:
when defined(tinyc): # == hasTinyCBackend; spelled out for the IC dep scanner
import tccgen
proc hcrOn(m: BModule): bool = m.config.hcrOn
@@ -61,9 +65,18 @@ proc addForwardedProc(m: BModule, prc: PSym) =
m.g.forwardedProcs.add(prc)
proc newModule*(g: BModuleList; module: PSym; conf: ConfigRef; idgen: IdGenerator): BModule
proc getCFile*(m: BModule): AbsoluteFile
proc findPendingModule(m: BModule, s: PSym): BModule =
# TODO fixme
if m.config.cmd == cmdNifC and m.config.icBackendStage == "cg":
# Per-module backend codegen: only module M (`m`) is emitted in this
# process, so every demanded definition — whether a normal proc owned by
# another (here unwritten) module or a minted instance/hook — is emitted
# into M's TU. Definitions owned elsewhere are emitted again by their own
# module's cg process; the merge stage keeps one per C name and turns the
# rest into prototypes (which already live in the unmarked protos section).
return m
if m.config.symbolFiles == v2Sf or optCompress in m.config.globalOptions:
let ms = s.itemId.module #getModule(s)
result = m.g.mods[ms]
@@ -71,15 +84,52 @@ proc findPendingModule(m: BModule, s: PSym): BModule =
var ms = getModule(s)
registerModule m.g.graph, ms
if ms.position >= m.g.mods.len:
result = newModule(m.g, ms, m.config, idGeneratorFromModule(ms))
result = newModule(m.g, ms, m.config, idGeneratorForBackend(ms))
else:
result = m.g.mods[ms.position]
if result == nil:
result = newModule(m.g, ms, m.config, idGeneratorFromModule(ms))
result = newModule(m.g, ms, m.config, idGeneratorForBackend(ms))
else:
var ms = getModule(s)
result = m.g.mods[ms.position]
proc icNifName(m: BModule; s: PSym): string =
## The serialized NIF name of `s`, recorded next to its C name in the cnif
## artifact so a later run can re-demand the definition when a reused TU
## still references it (the def-retention check). Backend-minted symbols
## have no NIF name.
if m.config.cmd == cmdNifC and s != nil and not isBackendMinted(s.itemId):
result = globalName(s, m.config)
else:
result = ""
proc icNifName(m: BModule; t: PType): string =
## The type flavor: recorded next to RTTI data definitions so the
## def-retention check can re-demand the typeinfo of a regenerating TU's
## previous artifact (`genTypeInfo` is type-driven, not symbol-driven).
if m.config.cmd == cmdNifC:
result = icNifTypeName(t, m.config)
else:
result = ""
proc emitsBodyInThisModule(m: BModule, prc: PSym): bool =
## Per-module backend codegen is concerned with ONE module: it emits the
## bodies of the routines that module OWNS (its own top-level defs) and only
## *prototypes* a routine owned by another module — that routine's body is
## emitted by its own module's `cg` process, and the merge stage's DCE prunes
## whatever ends up globally dead. The funnel where the main module re-emitted
## its entire transitive closure (≈1.8 GB, a 56 MB `.c.nif`) is exactly this
## rule being absent.
##
## Generic instances and synthesized hooks (`=destroy`, `$`, …) have no single
## owning-module top-level — they are minted on demand — so each demander emits
## them and the merge stage deduplicates by their content-addressed C name.
if not (m.config.cmd == cmdNifC and m.config.icBackendStage == "cg"):
return true
result = prc.itemId.module == m.module.position or
(prc.disamb and (InstanceDisambBit or HookDisambBit)) != 0'i32
proc initLoc(k: TLocKind, lode: PNode, s: TStorageLoc, flags: TLocFlags = {}): TLoc =
result = TLoc(k: k, storage: s, lode: lode,
snippet: "", flags: flags)
@@ -124,8 +174,6 @@ proc useHeader(m: BModule, sym: PSym) =
proc cgsym(m: BModule, name: string)
proc cgsymValue(m: BModule, name: string): Rope
proc getCFile(m: BModule): AbsoluteFile
proc getModuleDllPath(m: BModule): Rope =
let (dir, name, ext) = splitFile(getCFile(m))
let filename = strutils.`%`(platform.OS[m.g.config.target.targetOS].dllFrmt, [name & ext])
@@ -756,6 +804,9 @@ proc assignGlobalVar(p: BProc, n: PNode; value: Rope) =
useHeader(p.module, s)
if lfNoDecl in s.loc.flags: return
if not containsOrIncl(p.module.declaredThings, s.id):
if p.config.cmd == cmdNifC and sfImportc notin s.flags:
p.module.icDataDefs.add (stripCnifMarks(s.loc.snippet),
icNifName(p.module, s))
if sfThread in s.flags:
declareThreadVar(p.module, s, sfImportc in s.flags)
if value != "":
@@ -1316,6 +1367,34 @@ proc genProcBody(p: BProc; procBody: PNode) =
p.blocks[0].sections[cpsInit].addCall(cgsymValue(p.module, "nimErrorFlag"))
proc genProcLvl3*(m: BModule, prc: PSym) =
if m.config.cmd == cmdNifC:
fillBackendName(m, prc)
if (prc.disamb and (InstanceDisambBit or HookDisambBit)) != 0'i32 and
containsOrIncl(m.emittedContentDefs, stripCnifMarks(prc.loc.snippet)):
# A different symbol already emitted a body under this content-addressed
# C name in this TU (same generic instance / hook minted in two source
# modules, both loaded here). Emitting a second body is a C redefinition;
# a prototype was already produced for it, so just stop.
return
if sfDispatcher in prc.flags and sfMainModule notin m.module.flags:
# A method dispatcher enumerates the whole program's method set: its
# body is synthesized by `generateIfMethodDispatchers` only after all
# modules have been generated, and its single definition is emitted
# into the main TU by `finishModule` (main is finished last and never
# reused, so the definition can never go stale inside a cached TU).
# Any demand before that point yields a prototype.
genProcPrototype(m, prc)
return
if prc.itemId.module != m.module.position and
not isBackendMinted(prc.itemId) and
(prc.typ == nil or prc.typ.callConv != ccInline) and
sfDispatcher notin prc.flags:
# this TU embeds a definition whose body lives in another module's
# NIF: record the impl dependency (the artifact's cdeps head) so the
# reuse gate re-checks that module's impl cookie. Inline bodies are
# already part of the iface cookie; dispatcher bodies are synthesized
# from the whole program and live in main, which never reuses.
m.icImplMods.incl prc.itemId.module
var p = newProc(prc, m)
var header = newBuilder("")
let isCppMember = m.config.backend == backendCpp and sfCppMember * prc.flags != {}
@@ -1436,7 +1515,37 @@ proc genProcLvl3*(m: BModule, prc: PSym) =
generatedProc.add(extract(p.s(cpsStmts)))
if optStackTrace in prc.options: generatedProc.add(deinitFrame(p))
generatedProc.add(returnStmt)
m.s[cfsProcs].add(extract(generatedProc))
if m.config.cmd == cmdNifC:
# definition directive for the cnif artifact: groups the proc's text
# under its name and carries the root-relevant flags. The end directive
# right after the text makes the definition self-delimiting, so raw
# cfsProcs emitters (NimMain block, trav markers, ...) never end up
# inside a definition's span.
var defFlags = ""
if sfExportc in prc.flags or sfConstructor in prc.flags: defFlags.add 'x'
if sfCompilerProc in prc.flags: defFlags.add 'c'
if prc.kind == skMethod or sfDispatcher in prc.flags: defFlags.add 'm'
if (prc.typ == nil or prc.typ.callConv != ccInline) and
sfDispatcher notin prc.flags:
# A unique program-wide definition: external linkage, so exactly one
# translation unit may embed its body and everyone else declares it.
# Each module's `cg` process emits the body (emit-everywhere); this flag
# tells the merge stage which definitions to assign a single owner and
# prototype in the rest. The complement — inline procs and method
# dispatchers — is emitted into every using TU (`static`/main-only) and
# must never be deduplicated.
defFlags.add 'u'
if not hasCnifMarks(prc.loc.snippet):
# The C name was not minted through `fillBackendName` (e.g. set by an
# `extern`/`rtl` pragma at sem time), so its uses are invisible to the
# artifact's liveness walk — conservatively keep the definition.
defFlags.add 'x'
m.s[cfsProcs].add(cnifDefDirective(stripCnifMarks(prc.loc.snippet), defFlags,
icNifName(m, prc)))
m.s[cfsProcs].add(extract(generatedProc))
m.s[cfsProcs].add(cnifEndDefs())
else:
m.s[cfsProcs].add(extract(generatedProc))
if isReloadable(m, prc):
m.s[cfsDynLibInit].add('\t')
m.s[cfsDynLibInit].addAssignmentWithValue(prc.loc.snippet):
@@ -1482,10 +1591,15 @@ proc genProcPrototype(m: BModule, sym: PSym) =
var header = newBuilder("")
var visibility: DeclVisibility = None
genProcHeader(m, sym, header, visibility, asPtr = asPtr, addAttributes = true)
# A prototype is not a *use*: strip the cnif name marks so the artifact's
# liveness walk does not see every forward-declared proc as referenced.
var headerText = extract(header)
if m.config.cmd == cmdNifC:
headerText = stripCnifMarks(headerText)
if asPtr:
m.s[cfsProcHeaders].addDeclWithVisibility(visibility):
# genProcHeader would give variable declaration, add it directly
m.s[cfsProcHeaders].add(extract(header))
m.s[cfsProcHeaders].add(headerText)
else:
let extraVis =
if sym.typ.callConv != ccInline and requiresExternC(m, sym):
@@ -1494,7 +1608,7 @@ proc genProcPrototype(m: BModule, sym: PSym) =
None
m.s[cfsProcHeaders].addDeclWithVisibility(extraVis):
m.s[cfsProcHeaders].addDeclWithVisibility(visibility):
m.s[cfsProcHeaders].add(extract(header))
m.s[cfsProcHeaders].add(headerText)
m.s[cfsProcHeaders].finishProcHeaderAsProto()
include inliner
@@ -1572,7 +1686,8 @@ proc genProcLvl2(m: BModule, prc: PSym) =
# which will actually become a function pointer
if isReloadable(m, prc):
genProcPrototype(q, prc)
genProcLvl3(q, prc)
if emitsBodyInThisModule(m, prc):
genProcLvl3(q, prc)
else:
fillProcLoc(m, prc.ast[namePos])
useHeader(m, prc)
@@ -1582,7 +1697,7 @@ proc requestConstImpl(p: BProc, sym: PSym) =
if genConstSetup(p, sym):
let m = p.module
# declare implementation:
var q = findPendingModule(m, sym)
let q = findPendingModule(m, sym)
if q != nil and not containsOrIncl(q.declaredThings, sym.id):
assert q.initProc.module == q
genConstDefinition(q, p, sym)
@@ -1606,6 +1721,12 @@ proc genProc(m: BModule, prc: PSym) =
if not containsOrIncl(m.g.generatedHeader.declaredThings, prc.id):
genProcLvl3(m.g.generatedHeader, prc)
proc requestProcDef*(m: BModule, prc: PSym) =
## Public demand entry: request `prc`'s definition; it is routed to the
## module that owns it and generated once, exactly as if some generated
## code had referenced it.
genProc(m, prc)
proc genVarPrototype(m: BModule, n: PNode) =
#assert(sfGlobal in sym.flags)
let sym = n.sym
@@ -1675,7 +1796,7 @@ proc getSomeNameForModule(conf: ConfigRef, filename: AbsoluteFile): Rope =
## Returns a mangled module name.
result = mangleModuleName(conf, filename).mangle
proc getSomeNameForModule(m: BModule): Rope =
proc getSomeNameForModule*(m: BModule): Rope =
## Returns a mangled module name.
assert m.module.kind == skModule
assert m.module.owner.kind == skPackage
@@ -2062,6 +2183,40 @@ proc registerModuleToMain(g: BModuleList; m: BModule) =
else:
g.otherModsInit.addCallStmt(init)
proc registerReusedModuleToMain*(g: BModuleList; m: BModule;
initRequired, datInitRequired: bool) =
## `registerModuleToMain` for a module whose cached translation unit is
## reused: the init/datInit presence comes from the artifact's meta head
## instead of the (never generated) sections. Mirrors the non-hcr path of
## `registerModuleToMain` — reuse is disabled when hcr is on.
let
init = m.getInitName
datInit = m.getDatInitName
if datInitRequired:
g.mainModProcs.addDeclWithVisibility(Private):
g.mainModProcs.addProcHeader(ccNimCall, datInit, CVoid, cProcParams())
g.mainModProcs.finishProcHeaderAsProto()
g.mainDatInit.addCallStmt(datInit)
if sfSystemModule in m.module.flags:
if emulatedThreadVars(m.config) and m.config.target.targetOS != osStandalone:
g.mainDatInit.addCallStmt(cgsymValue(m, "initThreadVarsEmulation"))
if m.config.target.targetOS != osStandalone and m.config.selectedGC notin {gcNone, gcArc, gcAtomicArc, gcOrc, gcYrc}:
g.mainDatInit.addCallStmt(cgsymValue(m, "initStackBottomWith"),
cCast(CPointer, cAddr("inner")))
if initRequired:
g.mainModProcs.addDeclWithVisibility(Private):
g.mainModProcs.addProcHeader(ccNimCall, init, CVoid, cProcParams())
g.mainModProcs.finishProcHeaderAsProto()
if sfMainModule in m.module.flags:
g.mainModInit.addCallStmt(init)
elif sfSystemModule in m.module.flags:
g.mainDatInit.addCallStmt(init) # systemInit right after systemDatInit
else:
g.otherModsInit.addCallStmt(init)
proc genDatInitCode(m: BModule) =
## this function is called in cgenWriteModules after all modules are closed,
## it means raising dependency on the symbols is too late as it will not propagate
@@ -2309,6 +2464,16 @@ proc genModule(m: BModule, cfile: Cfile): Rope =
moduleIsEmpty = false
res.add(extract(m.s[i]))
# what `registerModuleToMain` will announce for this module; recorded in
# the artifact's meta head so a later run can reuse the TU
let initRequired = m.s[cfsInitProc].buf.len > 0
let datInitRequired = m.s[cfsDatInitProc].buf.len > 0
if m.config.cmd == cmdNifC:
# close the definitions section: the init procs that follow belong to
# the artifact's top level (always-run code, hence liveness roots)
res.add(cnifEndDefs())
if m.s[cfsInitProc].buf.len > 0:
moduleIsEmpty = false
res.add(extract(m.s[cfsInitProc]))
@@ -2331,6 +2496,22 @@ proc genModule(m: BModule, cfile: Cfile): Rope =
postprocessCode(m.config, result)
if m.config.cmd == cmdNifC and result.len > 0:
let artifact = cfile.cname.string & ".nif"
var implDeps: seq[string] = @[]
for pos in m.icImplMods.items:
if pos != m.module.position:
implDeps.add modname(pos, m.config)
sort implDeps
writeCnifArtifact(result, artifact, initRequired, datInitRequired,
m.icDataDefs,
semmedNif = toNifFilename(m.config, FileIndex m.module.position),
moduleBase = getSomeNameForModule(m),
implDeps = implDeps)
m.g.graph.icCnifFiles.add artifact
# NB: under cmdNifC the returned text still carries the cnif marks; the
# caller renders it (dropping dead definitions) or strips it.
proc initProcOptions(m: BModule): TOptions =
let opts = m.config.options
if sfSystemModule in m.module.flags: opts-{optStackTrace} else: opts
@@ -2342,6 +2523,8 @@ proc rawNewModule(g: BModuleList; module: PSym, filename: AbsoluteFile): BModule
result.headerFiles = @[]
result.declaredThings = initIntSet()
result.declaredProtos = initIntSet()
result.emittedContentDefs = initHashSet[string]()
result.icImplMods = initIntSet()
result.cfilename = filename
result.filename = filename
result.typeCache = initTable[SigHash, Rope]()
@@ -2413,10 +2596,13 @@ proc writeHeader(m: BModule) =
result.finishProcHeaderAsProto()
if m.config.cppCustomNamespace.len > 0: closeNamespaceNim(result)
result.addf("#endif /* $1 */$n", [guard])
if not writeRope(extract(result), m.filename):
var headerText = extract(result)
if m.config.cmd == cmdNifC:
headerText = stripCnifMarks(headerText)
if not writeRope(headerText, m.filename):
rawMessage(m.config, errCannotOpenFile, m.filename.string)
proc getCFile(m: BModule): AbsoluteFile =
proc getCFile*(m: BModule): AbsoluteFile =
let ext =
if m.compileToCpp: ".nim.cpp"
elif m.config.backend == backendObjc or sfCompileToObjc in m.module.flags: ".nim.m"
@@ -2510,8 +2696,9 @@ proc shouldRecompile(m: BModule; code: Rope, cfile: Cfile): bool =
rawMessage(m.config, errCannotOpenFile, cfile.cname.string)
result = true
proc writeModule(m: BModule) =
let cfile = getCFile(m)
proc genModuleCode(m: BModule; cf: var Cfile): string =
## First half of `writeModule`: finalizes the module and produces its code
## text. Under cmdNifC the text still carries the cnif marks.
if moduleHasChanged(m.g.graph, m.module):
genInitCode(m)
@@ -2526,9 +2713,11 @@ proc writeModule(m: BModule) =
m.s[cfsProcHeaders].add(extract(m.g.mainModProcs))
generateThreadVarsSize(m)
var cf = Cfile(nimname: m.module.name.s, cname: cfile,
obj: completeCfilePath(m.config, toObjFile(m.config, cfile)), flags: {})
var code = genModule(m, cf)
result = genModule(m, cf)
proc registerModuleCode(m: BModule; cf: var Cfile; code: string) =
## Second half of `writeModule`: writes the .c file if it changed and
## registers it for compilation.
if code != "" or m.config.symbolFiles != disabledSf:
when hasTinyCBackend:
if m.config.cmd == cmdTcc:
@@ -2538,6 +2727,15 @@ proc writeModule(m: BModule) =
if not shouldRecompile(m, code, cf): cf.flags = {CfileFlag.Cached}
addFileToCompile(m.config, cf)
proc writeModule(m: BModule) =
let cfile = getCFile(m)
var cf = Cfile(nimname: m.module.name.s, cname: cfile,
obj: completeCfilePath(m.config, toObjFile(m.config, cfile)), flags: {})
var code = genModuleCode(m, cf)
if m.config.cmd == cmdNifC:
code = stripCnifMarks(code)
registerModuleCode(m, cf, code)
proc updateCachedModule(m: BModule) =
let cfile = getCFile(m)
var cf = Cfile(nimname: m.module.name.s, cname: cfile,
@@ -2623,7 +2821,12 @@ proc finalCodegenActions*(graph: ModuleGraph; m: BModule; n: PNode) =
if m.g.forwardedProcs.len == 0:
incl m.flags, objHasKidsValid
if optMultiMethods in m.g.config.globalOptions or
if m.config.cmd == cmdNifC:
# nifbackend synthesizes the dispatchers between the module loop
# and the finish loop (emitMethodDispatchers): TUs demand-created
# by the dispatcher bodies must still reach `modulesClosed`
discard
elif optMultiMethods in m.g.config.globalOptions or
m.g.config.selectedGC notin {gcArc, gcOrc, gcAtomicArc, gcYrc} or
vtables notin m.g.config.features:
generateIfMethodDispatchers(graph, m.idgen)
@@ -2637,9 +2840,8 @@ proc genForwardedProcs(g: BModuleList) =
# a second pass here
# Note: ``genProcLvl2`` may add to ``forwardedProcs``
while g.forwardedProcs.len > 0:
let
prc = g.forwardedProcs.pop()
m = g.mods[prc.itemId.module]
let prc = g.forwardedProcs.pop()
let m = g.mods[prc.itemId.module]
if sfForward in prc.flags:
internalError(m.config, prc.info, "still forwarded: " & prc.name.s)
@@ -2654,7 +2856,32 @@ proc cgenWriteModules*(backend: RootRef, config: ConfigRef) =
# order anyway)
genForwardedProcs(g)
for m in cgenModules(g):
m.writeModule()
if config.cmd == cmdNifC and not isDefined(config, "icNoCDce"):
# Two-phase write: produce every module's marked text and artifact
# first, then compute global liveness over the artifacts and render
# the .c files with dead definitions dropped. Demand-driven codegen
# over-approximates (it cannot retract a definition once some path
# requested it); this is where the surplus is removed.
var mods: seq[BModule] = @[]
var cfs: seq[Cfile] = @[]
var codes: seq[string] = @[]
for m in cgenModules(g):
let cfile = getCFile(m)
var cf = Cfile(nimname: m.module.name.s, cname: cfile,
obj: completeCfilePath(m.config, toObjFile(m.config, cfile)), flags: {})
let code = genModuleCode(m, cf)
mods.add m
cfs.add cf
codes.add code
let cl = computeLiveFromCArtifacts(g.graph.icCnifFiles)
var dropped = 0
for i in 0..<mods.len:
let rendered =
if cl.broken: stripCnifMarks(codes[i])
else: renderMarkedC(codes[i], cl.live, dropped)
registerModuleCode(mods[i], cfs[i], rendered)
else:
for m in cgenModules(g):
m.writeModule()
writeMapping(config, g.mapping)
if g.generatedHeader != nil: writeHeader(g.generatedHeader)

View File

@@ -158,6 +158,12 @@ type
forwTypeCache*: TypeCache # cache for forward declarations of types
declaredThings*: IntSet # things we have declared in this .c file
declaredProtos*: IntSet # prototypes we have declared in this .c file
emittedContentDefs*: HashSet[string]
# cmdNifC per-module backend: content-addressed C names (generic
# instances and synthesized hooks) whose body this TU already emitted.
# Distinct symbols (minted in different source modules) can share one
# `_i<disamb>` name; `declaredThings` keys on symbol id and lets the
# second one through, so we dedup the body by name here instead.
queue*: seq[PSym] # queue of procs to generate
alive*: IntSet # symbol IDs of alive data as computed by `dce.nim`
headerFiles*: seq[string] # needed headers to include
@@ -176,6 +182,17 @@ type
extensionLoaders*: array['0'..'9', Builder] # special procs for the
# OpenGL wrapper
sigConflicts*: CountTable[SigHash]
icImplMods*: IntSet # module ids whose routine BODIES this TU
# embeds (redirected defs, shared instances,
# hooks); recorded as the artifact's cdeps so
# the reuse gate can check their impl cookies
icDataDefs*: seq[tuple[cname, nifname: string]]
# C names of data definitions (consts, globals,
# RTTI) this TU embeds plus their NIF symbol
# names (empty for RTTI, which has no symbol);
# recorded in the cnif artifact so a later run
# can reuse the TU and re-demand definitions
# that cached TUs still reference
g*: BModuleList
template config*(m: BModule): ConfigRef = m.g.config

View File

@@ -180,6 +180,7 @@ proc methodDef*(g: ModuleGraph; idgen: IdGenerator; s: PSym) =
g.methods[i].methods[0] != s:
# already exists due to forwarding definition?
localError(g.config, s.info, "method is not a base")
logMethodDef(g, s)
return
of No: discard
of Invalid:
@@ -191,6 +192,7 @@ proc methodDef*(g: ModuleGraph; idgen: IdGenerator; s: PSym) =
else:
g.bucketTable.inc(s.typ.firstParamType.skipTypes(skipPtrs).itemId)
g.methods.add((methods: @[s], dispatcher: createDispatcher(s, g, idgen)))
logMethodDef(g, s)
#echo "adding ", s.info
if witness != nil:
localError(g.config, s.info, "invalid declaration order; cannot attach '" & s.name.s &

View File

@@ -167,6 +167,8 @@ type
curExcSym: PSym # Current exception
externExcSym: PSym # Extern exception: what would getCurrentException() return outside of closure iter
enclosingPragmas: seq[PNode] # stack of pragma blocks wrapping stmtlist
states: seq[State] # The resulting states. Label is int literal.
finallyPathStack: seq[FinallyTarget] # Stack of split blocks, whiles and finallies
stateLoopLabel: PSym # Label to break on, when jumping between states.
@@ -252,7 +254,8 @@ proc newCurExcAccess(ctx: var Ctx): PNode =
ctx.newEnvVarAccess(ctx.curExcSym)
proc newStateLabel(ctx: Ctx): PNode =
ctx.g.newIntLit(TLineInfo(), 0)
result = nkIntLit.newIntNode(0)
result.typ = getSysType(ctx.g, TLineInfo(), tyInt16)
proc newState(ctx: var Ctx, n: PNode, inlinable: bool, label: PNode): PNode =
# Creates a new state, adds it to the context
@@ -592,10 +595,7 @@ proc lowerStmtListExprs(ctx: var Ctx, n: PNode, needsSplit: var bool): PNode =
let branch = n[i]
case branch.kind
of nkExceptBranch:
if branch[0].kind == nkType:
branch[1] = ctx.convertExprBodyToAsgn(branch[1], tmp)
else:
branch[0] = ctx.convertExprBodyToAsgn(branch[0], tmp)
branch[^1] = ctx.convertExprBodyToAsgn(branch[^1], tmp)
of nkFinally:
discard
else:
@@ -985,9 +985,14 @@ proc transformClosureIteratorBody(ctx: var Ctx, n: PNode, gotoOut: PNode): PNode
for j in i + 1..<n.len:
s.add(n[j])
var body = s
for pragma in ctx.enclosingPragmas:
body = newTreeI(nkPragmaBlock, n[i + 1].info,
pragma[0].copyTree, body)
n.sons.setLen(i + 1)
discard ctx.newState(s, true, label)
if ctx.transformClosureIteratorBody(s, gotoOut) != s:
discard ctx.newState(body, true, label)
if ctx.transformClosureIteratorBody(body, gotoOut) != body:
internalError(ctx.g.config, "transformClosureIteratorBody != s")
break
else:
@@ -1125,6 +1130,14 @@ proc transformClosureIteratorBody(ctx: var Ctx, n: PNode, gotoOut: PNode): PNode
finallyBody = ctx.transformClosureIteratorBody(finallyBody, finallyExit)
dec ctx.curFinallyLevel
of nkPragmaBlock:
# Propagate the pragma blocks so that blocks like {.cast(uncheckedAssign).}
# remain effective
ctx.enclosingPragmas.add(n)
n[1] = ctx.transformClosureIteratorBody(n[1], gotoOut)
discard ctx.enclosingPragmas.pop()
result = n
of nkGotoState, nkForStmt:
internalError(ctx.g.config, "closure iter " & $n.kind)

726
compiler/cnif.nim Normal file
View File

@@ -0,0 +1,726 @@
#
#
# The Nim Compiler
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## The "cnif" artifact: the C code generator's output as a NIF file.
##
## This is deliberately *not* NIFC: the C text is kept verbatim (Nim's
## C-level machinery — exception handling in particular — is more refined
## than what NIFC models today; the gap can be closed incrementally later).
## The only structure the artifact adds is the part dead code elimination
## and generic-instance merging need:
##
## - raw C text as string literals
## - every *global* entity's C name as a `Symbol` token
## - every emitted proc definition as a `(cdef SymbolDef flags ...)` group
##
## The C generator marks names with control characters at the single place
## a global's C name is minted (`fillBackendName`) and emits a definition
## directive at the single place finished procs are appended; the marks then
## ride through all of the snippet composition untouched. This module turns
## the final marked module text into the `.c.nif` artifact and strips the
## marks for the actual `.c` output. Rendering C from the artifact is a
## plain token walk: string literals verbatim, symbols by name — which is
## also where a later merge step redirects losing generic instances.
##
## Marker scheme (cannot collide: C string literals escape control chars,
## and `\1`/`\31`/`\23` of cgen's postprocess directives are distinct):
## \2 name \3 a global's C name
## \4 name \31 flags \31 nif \5 start of the definition of `name`;
## `nif` is the defining symbol's NIF name
## (empty for backend-minted symbols) so a
## later run can re-demand the definition
## \4 \5 end of the definitions section
import std / [tables, sets, os, assertions, syncio, algorithm]
import "../dist/nimony/src/lib" / [nifbuilder, nifcoreparse]
const
CnifSymStart* = '\2'
CnifSymEnd* = '\3'
CnifDefStart* = '\4'
CnifDefSep* = '\31' # same separator char as cgen's postprocess directives
CnifDefEnd* = '\5'
proc markCName*(name: string): string {.inline.} =
CnifSymStart & name & CnifSymEnd
proc hasCnifMarks*(s: string): bool =
for c in s:
if c in {CnifSymStart, CnifSymEnd, CnifDefStart}: return true
false
proc stripCnifMarks*(s: string): string =
## Removes the symbol marks (keeping the names) and the definition
## directives (entirely) so the result is plain C.
if not hasCnifMarks(s): return s
result = newStringOfCap(s.len)
var i = 0
while i < s.len:
case s[i]
of CnifSymStart, CnifSymEnd:
inc i
of CnifDefStart:
while i < s.len and s[i] != CnifDefEnd: inc i
inc i # skip CnifDefEnd
else:
result.add s[i]
inc i
const
CnifVersion* = "4"
## 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
## RTTI references the typeinfo flavor of the def-retention check
## needs (v4); `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
proc cnifEndDefs*(): string =
CnifDefStart & CnifDefEnd
proc writeCnifArtifact*(code: string; outfile: string;
initRequired = false; datInitRequired = false;
dataDefs: openArray[tuple[cname, nifname: string]] = [];
semmedNif = ""; moduleBase = "";
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
## 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
## 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
## check consults when some *other* TU regenerates), and a
## `(cdeps Ident*)` group naming the modules whose routine *bodies* this
## TU embeds (redirected defs, shared instances, hooks): the fine-grained
## reuse gate checks their `.impl.nif` cookies on top of the direct
## imports' `.iface.nif` cookies.
# pre-pass: every marked name is a use, every definition directive (and
# every data def) is a definition; external references = uses - defs
var uses = initHashSet[string]()
var defs = initHashSet[string]()
block prePass:
var i = 0
while i < code.len:
case code[i]
of CnifSymStart:
inc i
var name = ""
while i < code.len and code[i] != CnifSymEnd:
name.add code[i]
inc i
inc i
uses.incl name
of CnifDefStart:
inc i
var payload = ""
while i < code.len and code[i] != CnifDefEnd:
payload.add code[i]
inc i
inc i
let sep = find(payload, CnifDefSep)
if sep > 0: defs.incl payload[0..<sep]
elif payload.len > 0: defs.incl payload
else:
inc i
for d in dataDefs: defs.incl d.cname
var crefs: seq[string] = @[]
for u in uses:
if u notin defs: crefs.add u
sort crefs
var b = nifbuilder.open(outfile)
b.withTree "stmts":
b.withTree "meta":
var metaFlags = ""
if initRequired: metaFlags.add 'i'
if datInitRequired: metaFlags.add 'd'
if metaFlags.len > 0: b.addIdent metaFlags
else: b.addEmpty
b.addStrLit semmedNif
b.addStrLit moduleBase
b.addStrLit CnifVersion
b.withTree "cdata":
for d in dataDefs:
b.addSymbolDef d.cname
b.addStrLit d.nifname
b.withTree "cref":
for r in crefs:
b.addIdent r
b.withTree "cdeps":
for s in implDeps:
b.addIdent s
var raw = ""
var inDef = false
template flushRaw() =
if raw.len > 0:
b.addStrLit raw
raw.setLen 0
var i = 0
while i < code.len:
case code[i]
of CnifSymStart:
flushRaw()
inc i
var name = ""
while i < code.len and code[i] != CnifSymEnd:
name.add code[i]
inc i
inc i # skip CnifSymEnd
b.addSymbol name, ""
of CnifDefStart:
flushRaw()
inc i
var payload = ""
while i < code.len and code[i] != CnifDefEnd:
payload.add code[i]
inc i
inc i # skip CnifDefEnd
if inDef:
b.endTree()
inDef = false
if payload.len > 0:
let sep = find(payload, CnifDefSep)
let name = if sep >= 0: payload[0..<sep] else: payload
var flags = if sep >= 0: payload[sep+1..^1] else: ""
var nifName = ""
let sep2 = find(flags, CnifDefSep)
if sep2 >= 0:
nifName = flags[sep2+1..^1]
flags = flags[0..<sep2]
b.addTree "cdef"
b.addSymbolDef name
if flags.len > 0: b.addIdent flags
else: b.addEmpty
b.addStrLit nifName
inDef = true
else:
raw.add code[i]
inc i
flushRaw()
if inDef:
b.endTree()
b.close()
proc renderMarkedC*(code: string; live: HashSet[string]; dropped: var int): string =
## Renders the final C text from the marked module text: symbol marks are
## removed (keeping the names — a later merge step substitutes them here),
## and definitions whose name is not in `live` are dropped entirely. Each
## definition is self-delimiting (genProcAux emits an end directive right
## after the proc's text), so text written by other emitters is never part
## of a definition's span and survives unconditionally.
result = newStringOfCap(code.len)
var i = 0
while i < code.len:
case code[i]
of CnifSymStart, CnifSymEnd:
inc i
of CnifDefStart:
var payload = ""
inc i
while i < code.len and code[i] != CnifDefEnd:
payload.add code[i]
inc i
inc i # skip CnifDefEnd
if payload.len > 0:
let sep = find(payload, CnifDefSep)
let name = if sep >= 0: payload[0..<sep] else: payload
if name notin live:
inc dropped
# drop the definition's text: everything up to its end directive
while i < code.len and code[i] != CnifDefStart: inc i
else:
result.add code[i]
inc i
# ---- Liveness over the artifact -------------------------------------------
proc symOrIdentName(c: Cursor): string {.inline.} =
if c.kind == Ident: strVal(c) else: symName(c)
type
CnifHeads* = object
## The cheap-to-parse part of an artifact that a later run needs in
## order to reuse the TU without regenerating it.
valid*: bool ## file parsed, carries the meta head and has
## the current format version
initRequired*: bool
datInitRequired*: bool
semmedNif*: string ## the semmed NIF this TU was generated from
moduleBase*: string ## the module's mangled base name
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
cdeps*: seq[string] ## module suffixes whose routine bodies this
## TU embeds (impl-cookie gated on reuse)
proc readCnifHeads*(f: string): CnifHeads =
## Reads `(meta ...)`, `(cdata ...)`, `(cref ...)` and the `(cdef ...)`
## head names from an artifact. Artifacts written by an older compiler
## (no meta head or a different format version) report `valid=false`.
result = CnifHeads()
if not fileExists(f): return
var pool = newPool()
var tags = newTagPool()
let stmtsTag = tags.registerTag("stmts")
let cdefTag = tags.registerTag("cdef")
let cdataTag = tags.registerTag("cdata")
let crefTag = tags.registerTag("cref")
let cdepsTag = tags.registerTag("cdeps")
let metaTag = tags.registerTag("meta")
var buf = parseFromFile(f, 1000, pool, tags)
var c = beginRead(buf)
if c.kind != TagLit or c.cursorTagId != stmtsTag:
endRead(c)
return
var version = ""
var sawMeta = false
c.loopInto:
if c.kind == TagLit:
if c.cursorTagId == metaTag:
sawMeta = true
var strIdx = 0
c.loopInto:
if c.kind == Ident:
for ch in strVal(c):
if ch == 'i': result.initRequired = true
elif ch == 'd': result.datInitRequired = true
inc c
elif c.kind == StrLit:
if strIdx == 0: result.semmedNif = strVal(c)
elif strIdx == 1: result.moduleBase = strVal(c)
elif strIdx == 2: version = strVal(c)
inc strIdx
inc c
else:
skip c
elif c.cursorTagId == cdataTag:
c.loopInto:
if c.kind == SymbolDef:
result.cdata.add (symName(c), "")
inc c
elif c.kind == StrLit:
if result.cdata.len > 0:
result.cdata[^1].nifname = strVal(c)
inc c
else:
skip c
elif c.cursorTagId == crefTag:
c.loopInto:
if c.kind in {Ident, Symbol, SymbolDef}:
result.crefs.add symOrIdentName(c)
inc c
else:
skip c
elif c.cursorTagId == cdepsTag:
c.loopInto:
if c.kind in {Ident, Symbol, SymbolDef}:
result.cdeps.add symOrIdentName(c)
inc c
else:
skip c
elif c.cursorTagId == cdefTag:
# fixed head: SymbolDef, flags (Ident or empty), NIF name StrLit;
# everything after that is the definition's body text
var state = 0
c.loopInto:
if c.kind == SymbolDef:
result.cdefs.add (symName(c), "")
state = 1
inc c
elif state == 1: # the flags field
state = 2
skip c
elif state == 2: # the NIF name
if c.kind == StrLit and result.cdefs.len > 0:
result.cdefs[^1].nifname = strVal(c)
state = 3
skip c
else:
skip c
else:
skip c
else:
skip c
endRead(c)
result.valid = sawMeta and version == CnifVersion
type
CnifLiveness* = object
defs*: int ## proc definitions emitted across all modules
liveDefs*: int ## of those, reachable from the roots
live*: HashSet[string] ## live C names
broken*: bool
proc computeLiveFromCArtifacts*(files: openArray[string]): CnifLiveness =
## dce1-style mark&sweep over the C-shaped artifacts: a `(cdef ...)`
## group is a definition (flags 'x'/'c'/'m' — exportc, compilerproc,
## method/dispatcher — make it a root), names at the top level (data,
## globals, init code) are roots, names inside a group are its uses.
## Because the artifact is *fully lowered* output, no conservative
## modelling is needed: every call the C code contains is a token here.
##
## NB: mangled C names contain no dots, so NIF's text reader classifies
## them as `Ident` rather than `Symbol`; the dialect therefore treats
## Ident tokens as name uses. Inside a `(cdef ...)` the flags ident is
## the one immediately following the SymbolDef; everything after is a use.
result = CnifLiveness(live: initHashSet[string]())
var pool = newPool()
var tags = newTagPool()
let stmtsTag = tags.registerTag("stmts")
let cdefTag = tags.registerTag("cdef")
let cdataTag = tags.registerTag("cdata")
let crefTag = tags.registerTag("cref")
let cdepsTag = tags.registerTag("cdeps")
let metaTag = tags.registerTag("meta")
var uses = initTable[string, HashSet[string]]()
var roots = initHashSet[string]()
var defs = initHashSet[string]()
for f in files:
if not fileExists(f):
result.broken = true
return
var buf = parseFromFile(f, 1000, pool, tags)
var c = beginRead(buf)
if c.kind != TagLit or c.cursorTagId != stmtsTag:
result.broken = true
endRead(c)
return
c.loopInto:
case c.kind
of Symbol, Ident:
roots.incl symOrIdentName(c)
inc c
of TagLit:
if c.cursorTagId == metaTag or c.cursorTagId == cdataTag or
c.cursorTagId == crefTag or c.cursorTagId == cdepsTag:
# bookkeeping for TU reuse, irrelevant for liveness
skip c
elif c.cursorTagId == cdefTag:
var owner = ""
var flagsSeen = false
c.loopInto:
case c.kind
of SymbolDef:
owner = symName(c)
defs.incl owner
flagsSeen = false
inc c
of Symbol, Ident:
let name = symOrIdentName(c)
if not flagsSeen:
# the flags field right after the SymbolDef
flagsSeen = true
for ch in name:
# 'd' marks a data definition (const/RTTI): never DCE'd, so it
# is a root whose body keeps its referenced procs live
if ch in {'x', 'c', 'm', 'd'}:
roots.incl owner
break
else:
uses.mgetOrPut(owner, initHashSet[string]()).incl name
inc c
of DotToken:
flagsSeen = true # empty flags field
inc c
else:
skip c
else:
c.loopInto:
if c.kind in {Symbol, Ident}:
roots.incl symOrIdentName(c)
inc c
else:
skip c
else:
skip c
endRead(c)
# mark & sweep
var work = newSeqOfCap[string](roots.len)
for r in roots: work.add r
while work.len > 0:
let s = work.pop()
if not result.live.containsOrIncl(s):
if uses.hasKey(s):
for dep in uses[s]:
if dep notin result.live:
work.add dep
result.defs = defs.len
for d in defs:
if d in result.live: inc result.liveDefs
# ---- The merge stage: liveness + owner assignment -------------------------
type
MergeDecision* = object
## What the per-module backend's `merge` stage computes from every
## module's `.c.nif` and what its `emit` stage consumes to render the
## final `.c` of one module.
live*: HashSet[string] ## globally reachable C names (dead cdefs
## are dropped from every module)
owners*: Table[string, string] ## for each `'u'`-flagged (unique,
## externally-linked) definition, the single
## artifact base name allowed to embed its
## body; every other module prototypes it
broken*: bool ## an artifact was missing or unparsable —
## the caller should fall back / regenerate
defs*, liveDefs*: int
proc computeMergeDecision*(files: openArray[string]): MergeDecision =
## One pass over every `.c.nif`: the same mark&sweep as
## `computeLiveFromCArtifacts` plus, per definition, owner assignment.
##
## Each `cg` process emits the body of every definition it demands
## (emit-everywhere), so the same externally-linked definition appears in
## several artifacts. A `'u'` flag on the `(cdef ...)` marks those that need
## exactly one owner, assigned here across processes: the owner is the
## lexicographically smallest artifact that emits it — a pure function of the
## claimant set, hence stable across rebuilds. Definitions without `'u'`
## (inline procs, dispatchers) are `static`/main-only and emitted into every
## using TU, so they get no owner entry and are never deduplicated.
result = MergeDecision(live: initHashSet[string](),
owners: initTable[string, string]())
var pool = newPool()
var tags = newTagPool()
let stmtsTag = tags.registerTag("stmts")
let cdefTag = tags.registerTag("cdef")
let cdataTag = tags.registerTag("cdata")
let crefTag = tags.registerTag("cref")
let cdepsTag = tags.registerTag("cdeps")
let metaTag = tags.registerTag("meta")
var uses = initTable[string, HashSet[string]]()
var roots = initHashSet[string]()
var defs = initHashSet[string]()
for f in files:
if not fileExists(f):
result.broken = true
return
let owner = extractFilename(f)
var buf = parseFromFile(f, 1000, pool, tags)
var c = beginRead(buf)
if c.kind != TagLit or c.cursorTagId != stmtsTag:
result.broken = true
endRead(c)
return
c.loopInto:
case c.kind
of Symbol, Ident:
roots.incl symOrIdentName(c)
inc c
of TagLit:
if c.cursorTagId == metaTag or c.cursorTagId == cdataTag or
c.cursorTagId == crefTag or c.cursorTagId == cdepsTag:
skip c
elif c.cursorTagId == cdefTag:
var ownerName = ""
var flagsSeen = false
var needsOwner = false
c.loopInto:
case c.kind
of SymbolDef:
ownerName = symName(c)
defs.incl ownerName
flagsSeen = false
inc c
of Symbol, Ident:
let name = symOrIdentName(c)
if not flagsSeen:
flagsSeen = true
for ch in name:
if ch in {'x', 'c', 'm'}: roots.incl ownerName
# 'u' = unique proc (DCE'd), 'd' = data (never DCE'd, hence a
# root); both need a single owner across the emit-everywhere
# processes
elif ch == 'u': needsOwner = true
elif ch == 'd':
needsOwner = true
roots.incl ownerName
else:
uses.mgetOrPut(ownerName, initHashSet[string]()).incl name
inc c
of DotToken:
flagsSeen = true # empty flags field
inc c
else:
skip c
if needsOwner and ownerName.len > 0:
# smallest claimant wins; ties impossible (one entry per name)
let prev = result.owners.getOrDefault(ownerName, "")
if prev.len == 0 or owner < prev:
result.owners[ownerName] = owner
else:
c.loopInto:
if c.kind in {Symbol, Ident}:
roots.incl symOrIdentName(c)
inc c
else:
skip c
else:
skip c
endRead(c)
var work = newSeqOfCap[string](roots.len)
for r in roots: work.add r
while work.len > 0:
let s = work.pop()
if not result.live.containsOrIncl(s):
if uses.hasKey(s):
for dep in uses[s]:
if dep notin result.live:
work.add dep
result.defs = defs.len
for d in defs:
if d in result.live: inc result.liveDefs
const MergeDecisionFile* = "ic.backend.merge.nif"
## Fixed name of the merge stage's output in the nimcache, read by `emit`.
proc writeMergeDecision*(outfile: string; d: MergeDecision) =
## Serializes the merge decision: `(merge (live Symbol*) (owners (own
## Symbol StrLit)*))`. C names are mangled (no dots) so they serialize as
## symbols; owner artifact base names go in string literals.
var live: seq[string] = @[]
for n in d.live: live.add n
sort live
var keys: seq[string] = @[]
for k in d.owners.keys: keys.add k
sort keys
var b = nifbuilder.open(outfile)
b.withTree "merge":
b.withTree "live":
for n in live: b.addSymbol n, ""
b.withTree "owners":
for k in keys:
b.withTree "own":
b.addSymbol k, ""
b.addStrLit d.owners[k]
b.close()
proc readMergeDecision*(f: string): MergeDecision =
## Reads back a `writeMergeDecision` file; `broken=true` if absent/unparsable.
result = MergeDecision(live: initHashSet[string](),
owners: initTable[string, string]())
if not fileExists(f):
result.broken = true
return
var pool = newPool()
var tags = newTagPool()
let mergeTag = tags.registerTag("merge")
let liveTag = tags.registerTag("live")
let ownersTag = tags.registerTag("owners")
let ownTag = tags.registerTag("own")
var buf = parseFromFile(f, 1000, pool, tags)
var c = beginRead(buf)
if c.kind != TagLit or c.cursorTagId != mergeTag:
result.broken = true
endRead(c)
return
c.loopInto:
if c.kind == TagLit and c.cursorTagId == liveTag:
c.loopInto:
if c.kind in {Symbol, Ident}:
result.live.incl symOrIdentName(c)
inc c
else:
skip c
elif c.kind == TagLit and c.cursorTagId == ownersTag:
c.loopInto:
if c.kind == TagLit and c.cursorTagId == ownTag:
var key = ""
c.loopInto:
if c.kind in {Symbol, Ident}:
key = symOrIdentName(c)
inc c
elif c.kind == StrLit:
if key.len > 0: result.owners[key] = strVal(c)
inc c
else:
skip c
else:
skip c
else:
skip c
endRead(c)
proc renderCFromArtifact*(artifact: string; d: MergeDecision; ownerId: string;
dropped: var int): string =
## The per-module backend's `emit` stage: render one module's final `.c` from
## its `.c.nif` and the merge decision. String literals are emitted verbatim,
## symbols by name; a `(cdef ...)` body is dropped when the name is dead, or
## when it is a `'u'` unique definition this module does not own. The body's
## prototype lives in the surrounding raw text (cgen emits a forward
## declaration for every *used* proc, independent of where the body lands), so
## a dropped body still leaves a valid declaration — no synthesis needed. The
## head groups (meta/cdata/cref/cdeps) carry no C text.
result = ""
if not fileExists(artifact): return
var pool = newPool()
var tags = newTagPool()
let stmtsTag = tags.registerTag("stmts")
let cdefTag = tags.registerTag("cdef")
var buf = parseFromFile(artifact, 1000, pool, tags)
var c = beginRead(buf)
if c.kind != TagLit or c.cursorTagId != stmtsTag:
endRead(c)
return
c.loopInto:
case c.kind
of StrLit:
result.add strVal(c)
inc c
of Symbol, Ident:
result.add symOrIdentName(c)
inc c
of TagLit:
if c.cursorTagId == cdefTag:
# fixed head: SymbolDef, flags (Ident or empty), nifname StrLit; the
# rest is the definition's body text. `state` counts past the head.
var name = ""
var isUnique = false
var isData = false
var keep = true
var state = 0
c.loopInto:
if state == 0 and c.kind == SymbolDef:
name = symName(c)
state = 1
inc c
elif state == 1: # the flags field (one token: Ident/Symbol or empty)
if c.kind in {Ident, Symbol}:
for ch in symOrIdentName(c):
if ch == 'u': isUnique = true
elif ch == 'd': isData = true
state = 2
inc c
elif state == 2: # the NIF name (one StrLit) — decide keep here
let owned = d.owners.getOrDefault(name, ownerId) == ownerId
keep =
if isData: owned # data: kept by its owner only
elif isUnique: (name in d.live) and owned
else: name in d.live # inline/dispatcher: per-TU
if not keep: inc dropped
state = 3
inc c
else: # body tokens
if keep:
if c.kind == StrLit: result.add strVal(c)
elif c.kind in {Symbol, Ident}: result.add symOrIdentName(c)
inc c
else:
# head groups (meta/cdata/cref/cdeps) carry no C text
skip c
else:
inc c
endRead(c)

View File

@@ -24,7 +24,7 @@ bootSwitch(usedMarkAndSweep, defined(gcmarkandsweep), "--gc:markAndSweep")
bootSwitch(usedGoGC, defined(gogc), "--gc:go")
bootSwitch(usedNoGC, defined(nogc), "--gc:none")
import std/[setutils, os, strutils, parseutils, parseopt, sequtils, strtabs, enumutils]
import std/[setutils, sets, os, strutils, parseutils, parseopt, sequtils, strtabs, enumutils]
import
msgs, options, nversion, condsyms, extccomp, platform,
wordrecg, nimblecmd, lineinfos, pathutils
@@ -508,6 +508,7 @@ proc parseCommand*(command: string): Command =
of "jsonscript": cmdJsonscript
of "nifc": cmdNifC # generate C from NIF files
of "ic": cmdIc # generate .build.nif for nifmake
of "icconfig": cmdIcConfig # produce the precompiled config artifact
else: cmdUnknown
proc setCmd*(conf: ConfigRef, cmd: Command) =
@@ -653,6 +654,18 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
conf: ConfigRef) =
var key = ""
var val = ""
# Record config-file switches so the `nim ic` driver can serialise them into a
# precompiled-config artifact and have its per-module child processes replay
# them instead of re-parsing the `nim.cfg` chain (and re-running `config.nims`
# in the VM) on every invocation. Only `passPP` (config-file) switches are
# captured; command-line switches are forwarded by the build graph as usual.
# Path-search switches are skipped: their net effect already lives in the
# resolved `searchPaths` the driver forwards as `--path`, and replaying their
# raw (often relative-to-config-dir) arguments here would misresolve.
if pass == passPP and switch.normalize notin
["path", "p", "nimblepath", "lazypath", "excludepath",
"nonimblepath", "clearnimblepath", "nimcache"]:
conf.icConfigSwitches.add (switch, arg)
case switch.normalize
of "eval":
expectArg(conf, switch, arg, pass, info)
@@ -923,6 +936,49 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
else: localError(conf, info, errOnOrOffExpectedButXFound % arg)
of "noimportdoc":
processOnOffSwitchG(conf, {optNoImportdoc}, arg, pass, info)
of "ismainmodule":
# `nim m` (IC) only: marks the single module being checked as the program's
# real entry point so that `isMainModule` and `when isMainModule:` resolve
# correctly even though every module is compiled with `sfMainModule` set.
conf.isMainModule = switchOn(arg)
of "icgroup":
# `nim m` only: register a module that belongs to the current strongly-
# connected import group, so it is compiled from source (not loaded from a
# precompiled NIF) and gets its own NIF written. `deps.nim` emits one
# `--icGroup:<path>` per member of a dependency cycle. The argument is an
# absolute .nim path produced by the dependency scanner.
expectArg(conf, switch, arg, pass, info)
if pass in {passCmd2, passPP}:
conf.icGroup.incl(canonicalizePath(conf, AbsoluteFile arg).string)
of "icproject":
# `nim m`/`nim nifc` only: the ORIGINAL project file (see options.icProject)
expectArg(conf, switch, arg, pass, info)
if pass in {passCmd2, passPP}:
conf.icProject = canonicalizePath(conf, AbsoluteFile arg).string
of "icpreparsedconfig":
# `nim m`/`nim nifc` only: path of the precompiled-config artifact (see
# options.icPreparsedConfig). Read in `passCmd1`, before `loadConfigs`, so
# config loading can replay it instead of re-parsing the `nim.cfg` chain.
expectArg(conf, switch, arg, pass, info)
conf.icPreparsedConfig = arg
of "icconfigout":
# `nim icconfig` only: where to write the precompiled config artifact (see
# options.icConfigOut). The `nim ic` driver spawns the producer with this.
expectArg(conf, switch, arg, pass, info)
conf.icConfigOut = arg
of "icbackendstage":
# `nim nifc` only: per-module backend stage, one of cg|merge|emit (see
# options.icBackendStage). Empty (switch unused) keeps the whole-program
# backend. Emitted by `deps.nim`'s backend build file.
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).
expectArg(conf, switch, arg, pass, info)
if pass in {passCmd2, passPP}:
conf.icBackendModule = arg
of "import":
expectArg(conf, switch, arg, pass, info)
if pass in {passCmd2, passPP}:

File diff suppressed because it is too large Load Diff

View File

@@ -48,6 +48,7 @@ proc genEnumToStrProc*(t: PType; info: TLineInfo; g: ModuleGraph; idgen: IdGener
n[resultPos] = newSymNode(res)
result.ast = n
incl result.flagsImpl, {sfFromGeneric, sfNeverRaises}
setHookDisamb(g, result, "$enumtostr", t)
proc searchObjCaseImpl(obj: PNode; field: PSym): PNode =
case obj.kind

View File

@@ -46,7 +46,7 @@ proc isLocation(n: PNode): bool = not n.isValue
proc isLet(n: PNode): bool =
if n.kind == nkSym:
if n.sym.kind in {skLet, skTemp, skForVar}:
if n.sym.kind in {skLet, skConst, skTemp, skForVar}: # guard immutable variables
result = true
elif n.sym.kind == skParam and skipTypes(n.sym.typ,
abstractInst).kind notin {tyVar}:

View File

@@ -19,8 +19,11 @@ import std/tables
when defined(nimPreviewSlimSystem):
import std/assertions
proc replayStateChanges*(module: PSym; g: ModuleGraph) =
let list = module.ast
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
## loaded module's `ast` is never reconstructed, so the caller passes the replay
## actions it parsed out of the module's NIF directly.
assert list != nil
assert list.kind == nkStmtList
for n in list:
@@ -64,8 +67,9 @@ proc replayStateChanges*(module: PSym; g: ModuleGraph) =
g.cacheTables[destKey] = initBTree[string, PNode]()
if not contains(g.cacheTables[destKey], key):
g.cacheTables[destKey].add(key, val)
else:
internalError(g.config, n.info, "key already exists: " & key)
# else: the same key was already replayed. Under IC the import closure is
# replayed (direct module + transitive deps), so the same registration can
# legitimately be reached twice; re-applying it is a no-op, not an error.
of "incl":
let destKey = n[1].strVal
let val = n[2]
@@ -86,3 +90,37 @@ proc replayStateChanges*(module: PSym; g: ModuleGraph) =
g.cacheSeqs[destKey].add val
else:
internalAssert g.config, false
proc replayBackendActions*(g: ModuleGraph; module: PSym; list: PNode) =
## Applies the backend-relevant replay actions (C compile/link directives)
## found in a NIF-loaded module's top-level statement list. The `nifc`
## backend loads modules without going through sem's `replayStateChanges`,
## so e.g. math's `{.passL: "-lm".}` was lost and the final link failed
## with undefined references. VM cache actions are deliberately NOT
## replayed here — codegen does not run macros.
if list == nil: return
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:
let cname = AbsoluteFile n[1].strVal
var cf = Cfile(nimname: splitFile(cname).name, cname: cname,
obj: AbsoluteFile n[2].strVal,
flags: {CfileFlag.External},
customArgs: n[3].strVal)
extccomp.addExternalFileToCompile(g.config, cf)
of "link":
extccomp.addExternalFileToLink(g.config, AbsoluteFile n[1].strVal)
of "passl":
extccomp.addLinkOption(g.config, n[1].strVal)
of "passc":
extccomp.addCompileOption(g.config, n[1].strVal)
of "localpassc":
extccomp.addLocalCompileOption(g.config, n[1].strVal,
toFullPathConsiderDirty(g.config, module.info.fileIndex))
of "cppdefine":
options.cppDefine(g.config, n[1].strVal)
else:
discard

283
compiler/icconfig.nim Normal file
View File

@@ -0,0 +1,283 @@
#
#
# The Nim Compiler
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Precompiled config for the incremental compiler (`nim ic`).
##
## `nim ic` builds the program by spawning one `nim m` child per module (or
## strongly-connected import group) plus a final `nim nifc`. Each child is a
## full Nim process, so each would normally re-read the whole `nim.cfg` chain
## *and* re-run `config.nims` through the VM — work that is identical for every
## child and, because of the VM run, far from free. With ~85 modules in the
## compiler itself that config work is paid ~85 times during `koch bootic`.
##
## The fix mirrors Nimony's `.cfg.nif`: the driver parses config once, records
## the net effect, and the children replay it. Every config-file switch funnels
## through `processSwitch(..., passPP, ...)` (`nimconf.parseAssignment` and the
## `switch()` callback in `scriptconfig`), so the recorded sequence of those
## switches, replayed in order, reproduces an identical `ConfigRef` without any
## file read or VM run. The one config side effect that does not go through
## `processSwitch` is `cppDefine` (it mutates `conf.cppDefines` directly), so the
## resolved set is serialised alongside.
##
## Path-search switches are deliberately excluded from the recording (see
## `commands.processSwitch`): their resolved result already lives in
## `conf.searchPaths`, which the driver forwards to every child as absolute
## `--path` arguments; replaying their raw, config-dir-relative arguments here
## would misresolve.
import options, commands, lineinfos, pathutils, msgs
import std/[algorithm, os, sets, osproc, times, streams, syncio]
import "../dist/nimony/src/lib" / [nifbuilder, nifcoreparse]
const
IcConfigVersion* = "2"
## Artifact format version. Bump on any layout change here so a child built
## by an older compiler rejects a stale artifact and falls back to normal
## config loading instead of replaying a format it cannot parse.
proc writeIcConfig*(conf: ConfigRef; outfile: string) =
## Serialise the resolved config (the config-file switches recorded during
## `loadConfigs`, the resolved `cppDefines`/`searchPaths`, the nimcache dir, and
## the list of config *source* files for staleness detection) into `outfile`.
## `OnlyIfChanged`: when the content is byte-identical to what is already on
## disk the file is left untouched so its mtime does not advance — otherwise
## every `nim ic` run would re-fire the whole nifmake graph (see `nifler`'s
## `produceConfig`, whose model this mirrors).
var b = nifbuilder.open(outfile, writeMode = OnlyIfChanged)
b.withTree "stmts":
b.withTree "meta":
b.addStrLit IcConfigVersion
b.withTree "sources":
# Every config file read while loading (nim.cfg chain + config.nims), so a
# later run can decide via mtimes whether this artifact is still current
# (see `sourcesChanged`).
for f in conf.configFiles:
b.addStrLit f.string
b.withTree "nimcache":
# Resolved build nimcache. Recorded (unlike the path-search switches) so the
# driver, which replays this artifact instead of parsing `nim.cfg`, still
# learns a `--nimcache:` set inside `nim.cfg` and builds in the right place.
b.addStrLit conf.nimcacheDir.string
b.withTree "cppdefines":
# HashSet iteration order is unspecified; sort so the artifact is
# byte-stable across runs (nifmake keys rebuilds off content changes).
var defs: seq[string] = @[]
for d in conf.cppDefines: defs.add d
sort defs
for d in defs: b.addStrLit d
b.withTree "searchpaths":
# The resolved (absolute) search paths. Path-search *switches* are skipped
# below because their raw arguments are config-dir-relative; the net effect
# lives here instead, so a replayer with no `--path` command-line arguments
# (the `nim ic` driver itself) still resolves imports. `nim m`/`nim nifc`
# children also receive these as forwarded `--path` args; the dedup on
# replay makes the overlap harmless.
for p in conf.searchPaths:
b.addStrLit p.string
b.withTree "switches":
for sw in conf.icConfigSwitches:
b.addTree "sw"
b.addStrLit sw.switch
b.addStrLit sw.arg
b.endTree()
b.close()
proc applyIcConfig*(conf: ConfigRef; infile: string): bool =
## Replay the precompiled config into `conf`. Returns false (and applies
## nothing meaningful) when the artifact is missing or written by a compiler
## with an incompatible format version, so the caller can fall back to reading
## the config files normally.
if not fileExists(infile): return false
var pool = newPool()
var tags = newTagPool()
let
stmtsTag = tags.registerTag("stmts")
metaTag = tags.registerTag("meta")
sourcesTag = tags.registerTag("sources")
nimcacheTag = tags.registerTag("nimcache")
cppTag = tags.registerTag("cppdefines")
pathsTag = tags.registerTag("searchpaths")
switchesTag = tags.registerTag("switches")
swTag = tags.registerTag("sw")
var buf = parseFromFile(infile, 1000, pool, tags)
var c = beginRead(buf)
if c.kind != TagLit or c.cursorTagId != stmtsTag:
endRead(c)
return false
var version = ""
var sawMeta = false
let info = unknownLineInfo
c.loopInto:
if c.kind == TagLit:
if c.cursorTagId == metaTag:
sawMeta = true
c.loopInto:
if c.kind == StrLit:
version = strVal(c)
inc c
else:
skip c
elif c.cursorTagId == nimcacheTag:
c.loopInto:
if c.kind == StrLit:
let nc = strVal(c)
# Only when nimcache was not already pinned on the command line: a
# `--nimcache:` argument the driver/child was launched with must win
# over whatever `nim.cfg` recorded into the artifact.
if nc.len > 0 and conf.nimcacheDir.isEmpty:
conf.nimcacheDir = AbsoluteDir(nc)
inc c
else:
skip c
elif c.cursorTagId == sourcesTag:
# Replay does not need the source list; it exists only for
# `sourcesChanged`. Skip the whole section.
skip c
elif c.cursorTagId == cppTag:
c.loopInto:
if c.kind == StrLit:
cppDefine(conf, strVal(c))
inc c
else:
skip c
elif c.cursorTagId == pathsTag:
c.loopInto:
if c.kind == StrLit:
# Append preserving the serialised order (which already reflects the
# driver's addPath insert-at-front sequence), deduping against any
# path a child already received via a forwarded `--path` argument.
let d = AbsoluteDir(strVal(c))
if not conf.searchPaths.contains(d): conf.searchPaths.add d
inc c
else:
skip c
elif c.cursorTagId == switchesTag:
c.loopInto:
if c.kind == TagLit and c.cursorTagId == swTag:
var sw = ""
var arg = ""
var idx = 0
c.loopInto:
if c.kind == StrLit:
if idx == 0: sw = strVal(c)
else: arg = strVal(c)
inc idx
inc c
else:
skip c
processSwitch(sw, arg, passPP, info, conf)
else:
skip c
else:
skip c
else:
skip c
endRead(c)
result = sawMeta and version == IcConfigVersion
proc sourcesChanged*(configFile: string): bool =
## True when the precompiled config at `configFile` is missing, malformed,
## written by an incompatible version, or any recorded config *source* file is
## newer than it (or has vanished) — i.e. the artifact must be regenerated.
## Mirrors nifler's `sourcesChanged`: the source list lives inside the artifact
## so this needs no out-of-band knowledge of which `nim.cfg`s were read.
if not fileExists(configFile): return true
let modtime = getLastModificationTime(configFile)
var pool = newPool()
var tags = newTagPool()
let
stmtsTag = tags.registerTag("stmts")
metaTag = tags.registerTag("meta")
sourcesTag = tags.registerTag("sources")
var buf = parseFromFile(configFile, 1000, pool, tags)
var c = beginRead(buf)
if c.kind != TagLit or c.cursorTagId != stmtsTag:
endRead(c)
return true
var version = ""
var depsChanged = false
c.loopInto:
if c.kind == TagLit and c.cursorTagId == metaTag:
c.loopInto:
if c.kind == StrLit:
version = strVal(c)
inc c
else:
skip c
elif c.kind == TagLit and c.cursorTagId == sourcesTag:
c.loopInto:
if c.kind == StrLit:
let dep = strVal(c)
if not fileExists(dep) or getLastModificationTime(dep) >= modtime:
depsChanged = true
inc c
else:
skip c
else:
skip c
endRead(c)
result = depsChanged or version != IcConfigVersion
proc produceIcConfig*(conf: ConfigRef) =
## The `cmdIcConfig` command. By the time it runs, the normal pipeline has
## already fully parsed the `nim.cfg` chain and run `config.nims`, so the
## resolved config is sitting in `conf`; just serialise it to `--o`.
let outPath = conf.icConfigOut
if outPath.len == 0:
rawMessage(conf, errGenerated, "icconfig: missing output path (--icConfigOut)")
return
createDir(parentDir(outPath))
writeIcConfig(conf, outPath)
proc ensureIcConfig*(conf: ConfigRef) =
## Driver-side (`cmdIc`). Make sure an up-to-date precompiled config exists,
## (re)producing it in a *separate* process when missing or stale, then point
## `conf.icPreparsedConfig` at it so the driver replays the very same config its
## `nim m`/`nim nifc` children will — perfect speed (config parsed at most once,
## skipped entirely when nothing changed) and consistency (one producer, every
## process replays its output). The artifact lives in the nimcache derived from
## the command line (pre-config-parse), which is the one the children are told;
## a `--nimcache:` set inside `nim.cfg` is recovered from the artifact itself.
let cacheDir = getNimcacheDir(conf).string
# Start from a clean cache when the on-disk NIF format stamp is absent or stale
# (see `icFormatVersion`). This must happen HERE, before the config artifact is
# produced — `commandIc` performs the same check later, but by then the artifact
# would already live in the cache and the wipe would delete it.
createDir(cacheDir)
let versionFile = cacheDir / "ic.version"
let stamp = if fileExists(versionFile): readFile(versionFile) else: ""
if stamp != icFormatVersion:
removeDir(cacheDir)
createDir(cacheDir)
writeFile(versionFile, icFormatVersion)
let outPath = cacheDir / "ic_config.cfg.nif"
if not fileExists(outPath) or sourcesChanged(outPath):
createDir(cacheDir)
# Re-invoke ourselves as the config producer: reuse this process's command
# line, dropping the command argument (`ic`) in favour of `icconfig` and the
# explicit output path, both BEFORE the project file (anything after the
# project is swallowed into `config.arguments` by `cmdLineRest`). The
# producer re-reads `nim.cfg` itself.
var pargs = @["icconfig", "--icConfigOut:" & outPath]
var droppedCmd = false
for a in commandLineParams():
if not droppedCmd and a.len > 0 and a[0] != '-':
droppedCmd = true # drop the original command token (`ic`)
else:
pargs.add a
let p = startProcess(getAppFilename(), args = pargs,
options = {poStdErrToStdOut})
let outp = p.outputStream.readAll()
let code = p.waitForExit()
p.close()
if code != 0 or not fileExists(outPath):
rawMessage(conf, errGenerated,
"failed to produce precompiled config (exit code " & $code & "):\n" & outp)
return
conf.icPreparsedConfig = outPath

View File

@@ -803,6 +803,23 @@ proc hasCustomDestructor(c: Con, t: PType): bool =
obj = skipTypes(obj.baseClass, abstractPtrs)
result = result or isCustomDestructor(c, obj)
const
exprBranchKinds = {nkStmtListExpr, nkBlockExpr, nkIfExpr, nkCaseStmt,
nkTryStmt, nkPragmaBlock}
proc distributeAsgn(asgnKind: TNodeKind; dest, ri: PNode; c: var Con; s: var Scope): PNode =
## Distributes an assignment ``dest = ri`` into the leaf expressions of
## ``ri`` when ``ri`` is an expression-based control flow construct. This
## avoids creating pointless intermediate temporaries (bug #25850). The
## descent is recursive so that nestings like ``block: ...; if c: a else: b``
## assign directly to ``dest`` instead of going through a temp per branch.
if ri.kind in exprBranchKinds:
template process(child, s): untyped =
distributeAsgn(asgnKind, dest, child, c, s)
handleNestedTempl(ri, process, willProduceStmt = true)
else:
result = newTree(asgnKind, dest, p(ri, c, s, consumed))
proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode; tmpFlags = {sfSingleUsedTemp}; inReturn = false): PNode =
if n.kind in {nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr, nkIfStmt,
nkIfExpr, nkCaseStmt, nkWhen, nkWhileStmt, nkParForStmt, nkTryStmt, nkPragmaBlock}:
@@ -1004,13 +1021,11 @@ proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode; tmpFlags = {sfSing
result = moveOrCopy(p(n[0], c, s, mode), n[1], c, s, flags)
elif isDiscriminantField(n[0]):
result = c.genDiscriminantAsgn(s, n)
elif n[1].kind in {nkStmtListExpr, nkBlockExpr, nkIfExpr, nkCaseStmt, nkTryStmt, nkPragmaBlock}:
elif n[1].kind in exprBranchKinds:
# Distribute the assignment into each branch to avoid
# creating pointless temporaries for expression-based control flow.
let dest = p(n[0], c, s, mode)
template process(child, s): untyped =
newTree(n.kind, dest, p(child, c, s, consumed))
handleNestedTempl(n[1], process, willProduceStmt = true)
result = distributeAsgn(n.kind, dest, n[1], c, s)
else:
result = copyNode(n)
result.add p(n[0], c, s, mode)

98
compiler/itemids.nim Normal file
View File

@@ -0,0 +1,98 @@
#
#
# The Nim Compiler
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## `ItemId` is the identity of a symbol or type: a `(module, item)` pair.
##
## The fields are private on purpose: the module half reserves bit 30 as the
## "backend minted" marker, so all construction and inspection has to go
## through this module's API and the marker bit can never leak into module
## indexing or arithmetic.
##
## Three id spaces coexist per module:
## - Semantic-phase and NIF-loader ids: `itemId(module, item)` with `item > 0`.
## - Backend-minted ids (IC codegen, `nim nifc`: transf labels and temps,
## lifted hooks): `backendItemId` sets `BackendModuleBit`, so these can
## never compare equal to a loader id even though both counters mint the
## same small `item` range in one process. They never cross a process
## boundary and must never be written to a NIF file.
## - Derived env/tuple-field ids (`lowerings.addField`): the source local's
## id with `item` negated. `derivedFieldId` preserves the backend marker,
## keeping the derivation collision-free for both id spaces above.
import std/hashes
when defined(nimPreviewSlimSystem):
import std/assertions
const
BackendModuleBit = 0x4000_0000'i32
# Bit 30 of the module field. Bit 31 stays clear so marked module values
# remain non-negative and cannot be mistaken for the special negative
# module ids like `PackageModuleId`.
PackageModuleId* = -3'i32
type
ItemId* = object
moduleBits: int32
itemBits: int32
proc itemId*(module, item: int32): ItemId {.inline.} =
assert module < 0 or (module and BackendModuleBit) == 0
ItemId(moduleBits: module, itemBits: item)
proc backendItemId*(module, item: int32): ItemId {.inline.} =
## An id minted during IC codegen; distinct from every `itemId` of the
## same module so that the loader's stub counter and the backend's counter
## cannot collide in id-keyed tables.
assert module >= 0 and (module and BackendModuleBit) == 0
ItemId(moduleBits: module or BackendModuleBit, itemBits: item)
proc module*(x: ItemId): int32 {.inline.} =
if x.moduleBits >= 0: x.moduleBits and not BackendModuleBit
else: x.moduleBits
proc item*(x: ItemId): int32 {.inline.} = x.itemBits
proc isBackendMinted*(x: ItemId): bool {.inline.} =
x.moduleBits >= 0 and (x.moduleBits and BackendModuleBit) != 0
proc derivedFieldId*(source: ItemId): ItemId {.inline.} =
## The id of the env/tuple field that `lowerings.addField` derives for a
## captured local: `item` negated, module bits (including the backend
## marker) preserved.
ItemId(moduleBits: source.moduleBits, itemBits: -abs(source.itemBits))
proc matchesDerivedFieldId*(field, source: ItemId): bool {.inline.} =
## Does `field` carry the id `derivedFieldId` would derive for `source`?
## `source` may itself already be the derived field id.
field.moduleBits == source.moduleBits and
field.itemBits == -abs(source.itemBits)
proc `==`*(a, b: ItemId): bool {.inline.} =
# raw bit comparison: a backend-minted id never equals a loader id
a.itemBits == b.itemBits and a.moduleBits == b.moduleBits
proc hash*(x: ItemId): Hash =
var h: Hash = hash(x.moduleBits)
h = h !& hash(x.itemBits)
result = !$h
proc `$`*(x: ItemId): string =
result = "(module: " & $x.module & ", item: " & $x.itemBits
if x.isBackendMinted: result.add ", backend"
result.add ")"
const
moduleShift = when defined(cpu32): 20 else: 24
proc toId*(a: ItemId): int {.inline.} =
## Packs an ItemId into a single int. Uses the raw module bits so the
## backend marker keeps the two id spaces disjoint (bit 30 shifts to
## bit 54; like the module/item split itself this needs a 64-bit int).
(a.moduleBits.int shl moduleShift) + a.itemBits.int

View File

@@ -164,9 +164,21 @@ proc getClosureIterResult*(g: ModuleGraph; iter: PSym; idgen: IdGenerator): PSym
incl(result.flagsImpl, sfUsed)
iter.ast.add newSymNode(result)
proc closureParams(routine: PSym): PNode =
## The formal parameters node lambda lifting reads and extends. In a
## from-source compilation `routine.ast[paramsPos]` and `routine.typ.n` are the
## very same node (see the `typ.n.len` based position math below). Under IC the
## loaded proc AST omits the parameters (they are kept only in `typ.n`), so
## restore the shared node here.
result = routine.ast[paramsPos]
if (result == nil or result.kind == nkEmpty) and routine.typ != nil and
routine.typ.n != nil and routine.ast.len > paramsPos:
result = routine.typ.n
routine.ast[paramsPos] = result
proc addHiddenParam(routine: PSym, param: PSym) =
assert param.kind == skParam
var params = routine.ast[paramsPos]
var params = closureParams(routine)
# -1 is correct here as param.position is 0 based but we have at position 0
# some nkEffect node:
param.position = routine.typ.n.len-1
@@ -177,7 +189,8 @@ proc addHiddenParam(routine: PSym, param: PSym) =
proc getEnvParam*(routine: PSym): PSym =
if routine.ast.isNil: return nil
let params = routine.ast[paramsPos]
let params = closureParams(routine)
if params == nil or params.len == 0: return nil
let hidden = lastSon(params)
if hidden.kind == nkSym and hidden.sym.kind == skParam and hidden.sym.name.s == paramName:
result = hidden.sym
@@ -294,6 +307,7 @@ proc markAsClosure(g: ModuleGraph; owner: PSym; n: PNode) =
elif not (owner.typ.isClosure or owner.isNimcall and not owner.isExplicitCallConv or isEnv):
localError(g.config, n.info, "illegal capture '$1' because '$2' has the calling convention: <$3>" %
[s.name.s, owner.name.s, $owner.typ.callConv])
unsealForTransform(owner.typ)
incl(owner.typ, tfCapturesEnv)
if not isEnv:
owner.typ.callConv = ccClosure

View File

@@ -711,6 +711,11 @@ proc useSeqOrStrOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
doAssert t.asink != nil
body.add newHookCall(c, t.asink, x, y)
of attachedDestructor:
when defined(icDbg):
if t.destructor == nil:
echo "MISSING destructor: ", typeToString(t), " kind=", t.kind,
" itemId=", t.itemId, " uniqueId=", t.uniqueId, " 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)
of attachedTrace:
@@ -1080,8 +1085,17 @@ proc ownedClosureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
proc fillBody(c: var TLiftCtx; t: PType; body, x, y: PNode) =
case t.kind
of tyNone, tyEmpty, tyVoid: discard
of tyUncheckedArray:
# An UncheckedArray has no known length, so it cannot be copied, moved or
# destroyed as a value: it only ever lives behind a pointer and its bytes
# are managed manually (element ops for seqs/strings go through the
# seq/string hooks, which know the length). Emitting `x = y` for it (as the
# pointer-like group below does) produces an assignment of an unsized array,
# which the C backend cannot lower (genAssignment: tyUncheckedArray). So all
# value hooks for it are no-ops.
discard
of tyPointer, tySet, tyBool, tyChar, tyEnum, tyInt..tyUInt64, tyCstring,
tyPtr, tyUncheckedArray, tyVar, tyLent:
tyPtr, tyVar, tyLent:
defaultOp(c, t, body, x, y)
of tyRef:
if c.g.config.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc}:
@@ -1221,6 +1235,7 @@ proc symDupPrototype(g: ModuleGraph; typ: PType; owner: PSym; kind: TTypeAttache
n[resultPos] = newSymNode(res)
result.ast = n
incl result.flagsImpl, {sfFromGeneric, sfGeneratedOp}
setHookDisamb(g, result, AttachedOpToStr[kind], typ)
proc symPrototype(g: ModuleGraph; typ: PType; owner: PSym; kind: TTypeAttachedOp;
info: TLineInfo; idgen: IdGenerator; isDiscriminant = false): PSym =
@@ -1267,6 +1282,10 @@ proc symPrototype(g: ModuleGraph; typ: PType; owner: PSym; kind: TTypeAttachedOp
if kind == attachedWasMoved:
incl result.flagsImpl, sfNoSideEffect
incl result.typ, tfNoSideEffect
if not isDiscriminant:
# discriminant destructors derive their body from the enclosing object
# AND the selected field; their key is set at the call site
setHookDisamb(g, result, AttachedOpToStr[kind], typ)
proc genTypeFieldCopy(c: var TLiftCtx; t: PType; body, x, y: PNode) =
let xx = genBuiltin(c, mAccessTypeField, "accessTypeField", x)
@@ -1359,6 +1378,7 @@ proc produceDestructorForDiscriminator*(g: ModuleGraph; typ: PType; field: PSym,
assert(typ.skipTypes({tyAlias, tyGenericInst}).kind == tyObject)
# discrimantor assignments needs pointers to destroy fields; alas, we cannot use non-var destructor here
result = symPrototype(g, field.typ, typ.owner, attachedDestructor, info, idgen, isDiscriminant = true)
setHookDisamb(g, result, "=destroy¦" & field.name.s & "¦" & $field.position, typ)
var a = TLiftCtx(info: info, g: g, kind: attachedDestructor, asgnForType: typ, idgen: idgen,
fn: result)
a.asgnForType = typ

View File

@@ -100,6 +100,7 @@ type
warnGlobalVarConstructorTemporary = "GlobalVarConstructorTemporary",
warnImplicitRangeConversion = "ImplicitRangeConversion",
warnSystemRangeConversion = "SystemRangeConversion",
warnInvalidCmpOp = "InvalidCmpOp",
# hints
hintSuccess = "Success", hintSuccessX = "SuccessX",
hintCC = "CC",
@@ -210,6 +211,7 @@ const
warnGlobalVarConstructorTemporary: "global variable '$1' initialization requires a temporary variable",
warnImplicitRangeConversion: "implicit range conversion $1",
warnSystemRangeConversion: "implicit range conversion $1",
warnInvalidCmpOp: "$1",
hintSuccess: "operation successful: $#",
# keep in sync with `testament.isSuccess`
hintSuccessX: "$build\n$loc lines; ${sec}s; $mem; proj: $project; out: $output",

View File

@@ -378,6 +378,9 @@ proc wrongRedefinition*(c: PContext; info: TLineInfo, s: string;
conflictsWith: TLineInfo, note = errGenerated) =
## Emit a redefinition error if in non-interactive mode
if c.config.cmd != cmdInteractive:
when defined(icDbgRefc):
echo "[icRedef] ", s
echo getStackTrace()
localError(c.config, info, note,
"redefinition of '$1'; previous declaration here: $2" %
[s, c.config $ conflictsWith])
@@ -459,6 +462,15 @@ proc openShadowScope*(c: PContext) =
symbols: initStrTable(),
depthLevel: c.scopeDepth)
proc rememberShadowDefs*(c: PContext) =
## bug #25693: a template/macro operand's local definitions are sem-checked in
## a shadow scope that is then discarded. Record those definitions so that a
## later re-emission (e.g. a captured `typed` fragment expanded more than once)
## can be detected as a redefinition rather than silently miscompiled.
for s in c.currentScope.symbols:
if s.kind in {skVar, skLet, skForVar} and {sfGenSym, sfWasGenSym} * s.flags == {}:
c.shadowDiscardedDefs.incl s.id
proc closeShadowScope*(c: PContext) =
## closes the shadow scope, but doesn't merge any of the symbols
## Does not check for unused symbols or missing forward decls since a macro

View File

@@ -207,7 +207,7 @@ proc lookupInRecord(n: PNode, id: ItemId): PSym =
if result != nil: return
else: discard
of nkSym:
if n.sym.itemId.module == id.module and n.sym.itemId.item == -abs(id.item): result = n.sym
if matchesDerivedFieldId(n.sym.itemId, id): result = n.sym
else: discard
proc addField*(obj: PType; s: PSym; cache: IdentCache; idgen: IdGenerator): PSym =
@@ -215,7 +215,7 @@ proc addField*(obj: PType; s: PSym; cache: IdentCache; idgen: IdGenerator): PSym
# This is hacky but the clean solution is much more complex than it looks.
var field = newSym(skField, getIdent(cache, s.name.s & $obj.n.len),
idgen, s.owner, s.info, s.options)
field.itemId = ItemId(module: s.itemId.module, item: -s.itemId.item)
field.itemId = derivedFieldId(s.itemId)
let t = skipIntLit(s.typ, idgen)
field.typ = t
if s.kind in {skLet, skVar, skField, skForVar}:
@@ -235,7 +235,7 @@ proc addUniqueField*(obj: PType; s: PSym; cache: IdentCache; idgen: IdGenerator)
if result == nil:
var field = newSym(skField, getIdent(cache, s.name.s & $obj.n.len), idgen,
s.owner, s.info, s.options)
field.itemId = ItemId(module: s.itemId.module, item: -s.itemId.item)
field.itemId = derivedFieldId(s.itemId)
let t = skipIntLit(s.typ, idgen)
field.typ = t
assert t.kind != tyTyped

View File

@@ -29,6 +29,7 @@ when defined(nimPreviewSlimSystem):
import ../dist/checksums/src/checksums/sha1
import pipelines
from icconfig import produceIcConfig
when not defined(nimKochBootstrap):
import nifbackend
@@ -419,9 +420,14 @@ proc mainCommand*(graph: ModuleGraph) =
# cmdM uses NIF files, not ROD files
graph.config.symbolFiles = disabledSf
setUseIc(true)
# vtable dispatch needs a whole-program vtable layout, which the
# per-module compilation model cannot provide (yet); methods dispatch
# through the classic if-chain dispatchers instead
excl conf.features, Feature.vtables
commandCheck(graph)
of cmdNifC:
setUseIc(true)
excl conf.features, Feature.vtables
# Generate C code from NIF files
wantMainModule(conf)
setOutFile(conf)
@@ -434,6 +440,11 @@ proc mainCommand*(graph: ModuleGraph) =
commandIc(conf)
else:
rawMessage(conf, errGenerated, "nim deps not available in bootstrap build")
of cmdIcConfig:
# Produce the precompiled config artifact for `nim ic` (config already
# parsed by the normal pipeline); a separate process spawned by the driver.
wantMainModule(conf)
produceIcConfig(conf)
of cmdParse:
wantMainModule(conf)
discard parseFile(conf.projectMainIdx, cache, conf)
@@ -450,7 +461,14 @@ proc mainCommand*(graph: ModuleGraph) =
of cmdUnknown, cmdNone, cmdIdeTools:
rawMessage(conf, errGenerated, "invalid command: " & conf.command)
if conf.errorCounter == 0 and conf.cmd notin {cmdTcc, cmdDump, cmdNop}:
if conf.errorCounter == 0 and conf.cmd notin {cmdTcc, cmdDump, cmdNop, cmdM} and
not (conf.cmd == cmdNifC and conf.icBackendStage.len > 0):
# The IC build runs hundreds of internal per-module child processes — the
# frontend `nim m` (cmdM) and the per-module backend stages (cg/emit/merge/
# link). Each would print a `[SuccessX]` summary that is pure noise (and
# misleading: `out: unknownOutput`, or `out: <the whole compiler>` for a
# step that only wrote one `.c.nif`/`.c`). The driving `nim ic` (and koch)
# reports the real result.
if optProfileVM in conf.globalOptions:
echo conf.dump(conf.vmProfileData)
genSuccessX(conf)

View File

@@ -53,7 +53,29 @@ proc mangleParamExt*(s: PSym): string =
result.addInt s.position
proc mangleProcNameExt*(graph: ModuleGraph, s: PSym): string =
result = "__"
# The disambiguator comes first and the module suffix LAST, so the suffix is
# a strippable trailing token: content-addressed cross-module merging chops
# everything from the final `__` to recover a mint-site-independent name.
if s.itemId.isBackendMinted:
# A symbol minted during IC codegen (`idGeneratorForBackend`): its idgen
# 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). These 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>`.
result = "_c"
result.addInt s.itemId.item
else:
result = "_u"
# Use `disamb` rather than `itemId.item`: under incremental compilation a
# symbol loaded from a NIF file gets a fresh, load-order-dependent `itemId.item`
# (from the per-module symbol counter), which is neither stable across the
# processes that compile vs. use a module nor guaranteed distinct from another
# loaded symbol's. `disamb` is assigned deterministically per (module, name)
# and, together with the already-prepended mangled name, yields a unique and
# stable C identifier.
result.addInt s.disamb
result.add "__"
result.add graph.ifaces[s.itemId.module].uniqueName
result.add "_u"
result.addInt s.itemId.item # s.disamb #

View File

@@ -11,7 +11,7 @@
## represents a complete Nim project. Single modules can either be kept in RAM
## or stored in a rod-file.
import std/[intsets, tables, hashes, strtabs, os, strutils, parseutils]
import std/[intsets, tables, hashes, strtabs, os, strutils, parseutils, sets]
import ../dist/checksums/src/checksums/md5
import ast, astalgo, options, lineinfos,idents, btrees, ropes, msgs, pathutils, packages, suggestsymdb
@@ -68,6 +68,42 @@ type
enumToStringProcs*: Table[ItemId, PSym]
loadedEnumToStringProcs: Table[string, PSym]
emittedTypeInfo*: Table[string, FileIndex]
instDisambs: Table[(int, int32), ItemId] # (name id, content disamb) ->
# instance, for collision probing in
# `setInstanceDisamb`
icCnifFiles*: seq[string] # `.c.nif` artifacts written by this run
pendingMethodReplays*: seq[PSym] # method registrations loaded under
# `nim nifc`, bucketed only after every
# module is loaded (`flushMethodReplays`)
icImplDeps*: IntSet # NeedsImpl edge tracking under `nim m`:
# module ids (FileIndex) whose routine BODIES
# this compilation consumed at compile time.
# Written to the `.edges` sidecar; deps.nim
# then gates the dependent on those modules'
# IMPL cookie instead of the iface cookie, so
# e.g. `const x = dep.foo()` re-sems when foo's
# body changes. Uniform across body-access
# kinds — the iface cookie hashes signatures
# ONLY (see ast2nif.cookieSd), so every body
# consumer records an edge here: VM-compiled /
# getImpl'ed bodies (recordIcImplDep from vm/
# vmgen), expanded templates (semTemplateExpr)
# and instantiated generics (generateInstance).
# Inline iterators / `inline` procs are NOT
# tracked: they are inlined at codegen, where
# the nifc backend's NIF-mtime invalidation
# already re-codegens their users.
icQualIfaces*: IntSet # module positions whose interface tables were
# populated ONLY for qualified access through a
# module re-export (`import x; export x`); the
# Iface.module stays nil so a later direct
# import still takes the full load path
inVMTransform*: int # >0 while the VM compiles a routine body
# (vmgen.genProc's transformBody): hooks lifted
# there (e.g. for closure-env types of LOADED
# routines) are process-local VM artifacts —
# serializing them would embed references to
# derived env-field syms that no module defines
packageSyms*: TStrTable
deps*: IntSet # the dependency graph or potentially its transitive closure.
@@ -110,6 +146,11 @@ type
cacheSeqs*: Table[string, PNode] # state that is shared to support the 'macrocache' API; IC: implemented
cacheCounters*: Table[string, BiggestInt] # IC: implemented
cacheTables*: Table[string, BTree[string, PNode]] # IC: implemented
transitiveReplayActions*: seq[PNode] # macro-cache replay actions collected from
# the transitive import closure of a NIF-loaded module (loadTransitiveHooks);
# the caller (pipelines) replays them so a dependency's macrocache state — e.g.
# nim-serialization's flavor registration — reaches a module that imports it
# only indirectly. Drained per moduleFromNifFile call.
passes*: seq[TPass]
pipelinePass*: PipelinePass
onDefinition*: proc (graph: ModuleGraph; s: PSym; info: TLineInfo) {.nimcall.}
@@ -126,6 +167,7 @@ type
procGlobals*: seq[PNode]
nifReplayActions*: Table[int32, seq[PNode]] # module position -> replay actions for NIF
cachedMods: IntSet
hookClosure: IntSet # modules whose serialized hooks were already registered
TPassContext* = object of RootObj # the pass's context
idgen*: IdGenerator
@@ -235,6 +277,18 @@ iterator allSyms*(g: ModuleGraph; m: PSym): PSym =
if s != nil:
yield s
proc reexportedModuleSyms*(g: ModuleGraph; m: PSym): seq[(string, string)] =
## (name, NIF module suffix) of MODULE syms in `m`'s interface — these are
## re-exports (`import x; export x`, added by `reexportSym`) acting as
## qualifiers (`m.x.sym`). Consumed by the NIF writer; semExport does not
## put them into the nkExportStmt children, so the AST walk cannot see them.
result = @[]
var seen = initIntSet()
for s in g.ifaces[m.position].interf.data:
if s != nil and s.kind == skModule and s.position != m.position and
not seen.containsOrIncl(s.position):
result.add (s.name.s, cachedModuleSuffix(g.config, FileIndex s.position))
proc someSym*(g: ModuleGraph; m: PSym; name: PIdent): PSym =
let importHidden = optImportHidden in m.options
result = strTableGet(g.ifaces[m.position].interfSelect(importHidden), name)
@@ -280,21 +334,67 @@ proc getAttachedOp*(g: ModuleGraph; t: PType; op: TTypeAttachedOp): PSym =
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
result = g.loadedOps[op].getOrDefault(key)
#echo "fallback ", key, " ", op, " ", result
when defined(icDbgHash):
if result == nil and op == attachedDestructor:
echo "HOOK MISS key=", key, " table.len=", g.loadedOps[op].len,
" kind=", t.kind, " sym=", (if t.sym != nil: t.sym.name.s else: "NIL")
if key.len > 10:
let probe = key[3 ..< min(key.len, 18)]
for k in g.loadedOps[op].keys:
if probe in k: echo " candidate: ", k
else:
result = nil
proc setAttachedOp*(g: ModuleGraph; module: int; t: PType; op: TTypeAttachedOp; value: PSym) =
## we also need to record this to the packed module.
if not g.attachedOps[op].contains(t.itemId):
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
# Use key-based deduplication for opsLog because different type objects
# (e.g. canon vs orig) can have different itemIds but same structural key
if key notin g.loadedOps[op]:
# Hooks should be written to the module where the type is defined,
# not the module that triggered the registration
let ownerModule = if t.sym != nil: t.sym.itemId.module.int else: module
g.opsLog.add LogEntry(kind: HookEntry, op: op, module: ownerModule, key: key, sym: value)
# Key-based deduplication for opsLog: different type objects (e.g. canon vs
# orig) can have different itemIds but the same structural key.
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
if g.inVMTransform > 0 and g.config.cmd == cmdM:
# hook lifted while the VM compiles a routine body (closure-env types of
# loaded routines): register it for in-process lookup but keep it out of
# the serialized log — it is a process-local artifact whose type graph
# 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
return
let existing = g.loadedOps[op].getOrDefault(key)
if existing == nil:
# Stamp the entry with the module whose compilation produced the hook
# (`module`), NOT the type's def module: each `nim m` is a separate
# process, so a hook lifted while compiling a *downstream* module simply
# does not exist in the def module's process — stamping it with the def
# module produced a `LogEntry` that no module ever writes (the def
# module's writer ran in another process that never lifted it; this
# module's writer skips it because `op.module != thisModule`) and codegen
# failed with "'=destroy' operator not found" (e.g. astdef's `TStrTable`,
# whose destroy is first needed by modulegraphs). This holds for nominal
# types as much as for generic/structural instances. Duplicate
# registrations across lifting modules are reconciled deterministically
# at load time (see the HookEntry replay in `replayStateChanges`).
g.opsLog.add LogEntry(kind: HookEntry, op: op, module: module, key: key, sym: value)
g.loadedOps[op][key] = value
elif existing != value:
# Re-registration replacing an earlier sym for the same key. This happens
# legitimately: `createTypeBoundOps` first registers empty `symPrototype`
# placeholders, then `produceSym` replaces them — in particular
# `produceSymDistinctType` replaces a distinct type's placeholder with the
# BASE type's hook (a `distinct string` uses string's `=sink`). The log
# must follow the replacement, otherwise the NIF ships the dead,
# empty-bodied prototype and codegen in another process calls a no-op
# `=sink`/`=copy`, silently losing the value (e.g. `conf.projectPath`
# ended up empty: "cannot open '/'").
g.loadedOps[op][key] = value
var updated = false
for e in mitems(g.opsLog):
if e.kind == HookEntry and e.op == op and e.key == key:
e.sym = value
e.module = module
updated = true
break
if not updated:
g.opsLog.add LogEntry(kind: HookEntry, op: op, module: module, key: key, sym: value)
g.attachedOps[op][t.itemId] = value
proc setAttachedOp*(g: ModuleGraph; module: int; typeId: ItemId; op: TTypeAttachedOp; value: PSym) =
@@ -343,8 +443,10 @@ proc getToStringProc*(g: ModuleGraph; t: PType): PSym =
proc setToStringProc*(g: ModuleGraph; t: PType; value: PSym) =
g.enumToStringProcs[t.itemId] = value
let key = typeKey(t, g.config, loadTypeCallback, loadSymCallback)
let ownerModule = if t.sym != nil: t.sym.itemId.module.int else: value.itemId.module.int
g.opsLog.add LogEntry(kind: EnumToStrEntry, module: ownerModule, key: key, sym: value)
# 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
# "written by nobody" failure as hook entries, see setAttachedOp).
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):
@@ -357,6 +459,49 @@ proc addMethodToGeneric*(g: ModuleGraph; module: int; t: PType; col: int; m: PSy
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)
proc logMethodDef*(g: ModuleGraph; s: PSym) =
## Log a method registration (`cgmeth.methodDef`) so that importers and
## the backend can rebuild the dispatch buckets (`g.methods`) from the
## NIF replay log — the serialized method ast carries its dispatcher sym
## at `dispatcherPos`, so replay reuses the original dispatcher that all
## call sites reference by name (see `registerLoadedMethod`).
if g.config.cmd in {cmdNifC, cmdM}:
g.opsLog.add LogEntry(kind: MethodEntry, module: s.itemId.module.int,
key: "", sym: s)
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
## does not exist in serialized form — `generateIfMethodDispatchers`
## synthesizes it in the backend from the complete bucket.
template dbg(msg: string) =
when defined(icDbgMeth):
echo "[icMeth] replay ", (if m != nil: m.name.s else: "nil"), ": ", msg
if m == nil or sfDispatcher in m.flags: dbg "skip self/nil"; return
if m.ast == nil or dispatcherPos >= m.ast.len:
dbg "no dispatcherPos (len " & $(if m.ast != nil: m.ast.len else: -1) & ")"
return
let dn = m.ast[dispatcherPos]
if dn == nil or dn.kind != nkSym or dn.sym == nil: dbg "empty dispatcher slot"; return
let disp = dn.sym
if sfDispatcher notin disp.flags: dbg "slot sym not a dispatcher"; return
dbg "ok -> bucket of " & disp.name.s & "." & $disp.disamb
for i in 0..<g.methods.len:
if g.methods[i].dispatcher.itemId == disp.itemId:
for existing in g.methods[i].methods:
if existing.itemId == m.itemId: return
g.methods[i].methods.add m
return
g.methods.add (methods: @[m], dispatcher: disp)
proc flushMethodReplays*(g: ModuleGraph) =
## Builds the dispatch buckets from the method registrations collected
## during module loading; called once every module of the program is
## loaded (`nifbackend.generateCode`).
for s in g.pendingMethodReplays:
registerLoadedMethod(g, s)
g.pendingMethodReplays.setLen 0
proc logGenericInstance*(g: ModuleGraph; inst: PSym) =
## Log a generic instance so it gets written to the NIF file.
## This is needed when generic instances are created during compile-time
@@ -365,6 +510,86 @@ 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]) =
## Under IC, replace a fresh routine instance's counter-based `disamb` with
## a content-derived one: a hash of the generic's identity plus the
## `typeKey` of every concrete type argument — exactly the identity the
## instantiation cache compares. The instance's NIF name
## `name.disamb.modsuffix` then differs only in the module suffix when the
## same instantiation is made by different modules, which is the
## prerequisite for cross-module generic-instance merging (and gives the
## dce analysis its `offers` keys). The hash is computed once, here; it is
## never recomputed — the value travels in the serialized `disamb` field.
if g.config.cmd notin {cmdNifC, cmdM}: return
if isDefined(g.config, "icNoInstKey"): return
var key = generic.name.s
key.add '.'
key.addInt generic.disamb
key.add '.'
key.add modname(generic.itemId.module, g.config)
for t in concreteTypes:
key.add '|'
key.add typeKey(t, g.config, loadTypeCallback, loadSymCallback)
let d = toMD5(key)
var h = (int32(d[0]) or (int32(d[1]) shl 8) or (int32(d[2]) shl 16) or
(int32(d[3] and 0x3F'u8) shl 24)) or InstanceDisambBit
# Same-name hash collisions inside this process get probed to the next
# free value; the loser stays correct (its name keeps the module suffix),
# it merely won't merge cross-module.
while true:
let probe = (inst.name.id, h)
if g.instDisambs.hasKey(probe):
if g.instDisambs[probe] == inst.itemId: break
h = if h == high(int32): InstanceDisambBit else: h + 1
else:
g.instDisambs[probe] = inst.itemId
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
## the type it is bound to. Counter disambs renumber whenever an *earlier*
## hook appears in a re-semmed module, so cached translation units keep
## calling the old `_u<disamb>` C name while the regenerated producer
## defines a new one — the hook flavor of the backend def-migration hole.
## With a content-derived value the hook's NIF name (and hence its C name)
## is stable as long as the type itself is unchanged.
if g.config.cmd notin {cmdNifC, cmdM}: return
if isDefined(g.config, "icNoHookKey"): return
var key = opName
key.add '|'
key.add typeKey(typ, g.config, loadTypeCallback, loadSymCallback)
let d = toMD5(key)
var h = (int32(d[0]) or (int32(d[1]) shl 8) or (int32(d[2]) shl 16) or
(int32(d[3] and 0x1F'u8) shl 24)) or HookDisambBit
# Same-name hash collisions inside this process get probed to the next
# free value (staying below InstanceDisambBit); the loser merely loses
# cross-run name stability.
while true:
let probe = (hook.name.id, h)
if g.instDisambs.hasKey(probe):
if g.instDisambs[probe] == hook.itemId: break
h = if h == InstanceDisambBit - 1'i32: HookDisambBit else: h + 1
else:
g.instDisambs[probe] = hook.itemId
break
hook.disamb = h
proc hasDisabledAsgn*(g: ModuleGraph; t: PType): bool =
let op = getAttachedOp(g, t, attachedAsgn)
result = op != nil and sfError in op.flags
@@ -543,6 +768,7 @@ proc initModuleGraphFields(result: ModuleGraph) =
result.emittedTypeInfo = initTable[string, FileIndex]()
result.cachedFiles = newStringTable()
result.cachedMods = initIntSet()
result.hookClosure = initIntSet()
proc newModuleGraph*(cache: IdentCache; config: ConfigRef): ModuleGraph =
result = ModuleGraph()
@@ -573,6 +799,15 @@ proc getModule*(g: ModuleGraph; fileIdx: FileIndex): PSym =
proc moduleOpenForCodegen*(g: ModuleGraph; m: FileIndex): bool {.inline.} =
result = true
proc recordIcImplDep*(g: ModuleGraph; s: PSym) =
## NeedsImpl edge tracking, see `icImplDeps`. Called from the compile-time
## body consumption sites (vmgen's proc compilation, the getImpl opcodes).
## Own-module and group-member entries are filtered out when the `.edges`
## sidecar is written.
if g.config.cmd == cmdM and s != nil and s.kind in routineKinds and
s.itemId.module >= 0 and not isBackendMinted(s.itemId):
g.icImplDeps.incl module(s.itemId).int
proc dependsOn(a, b: int): int {.inline.} = (a shl 15) + b
proc addDep*(g: ModuleGraph; m: PSym, dep: FileIndex) =
@@ -658,6 +893,105 @@ proc getBody*(g: ModuleGraph; s: PSym): PNode {.inline.} =
assert result != nil
when not defined(nimKochBootstrap):
proc registerLoadedHooks(g: ModuleGraph; logOps: seq[LogEntry]) =
let mainSuffix = getMainModuleSuffix(ast.program)
for x in logOps:
# A dependency's NIF may carry hooks whose syms belong to the module we
# are compiling fresh (e.g. a stale NIF of that very module written by an
# earlier in-process compilation). Loading those would collide with the
# freshly semchecked hook declarations.
if mainSuffix.len > 0 and
cachedModuleSuffix(g.config, x.sym.itemId.module.FileIndex) == mainSuffix:
continue
case x.kind
of HookEntry:
# The same structural hook may be serialized by several instantiating
# modules (a generic/structural instance has no single def site, so each
# using module owns its copy). Pick one deterministic program-wide winner
# by the smaller owning-module name, so every lookup resolves to the same
# sym regardless of module load order.
let existing = g.loadedOps[x.op].getOrDefault(x.key)
if existing == nil or
cachedModuleSuffix(g.config, x.sym.itemId.module.FileIndex) <
cachedModuleSuffix(g.config, existing.itemId.module.FileIndex):
g.loadedOps[x.op][x.key] = x.sym
of EnumToStrEntry:
g.loadedEnumToStringProcs[x.key] = x.sym
of MethodEntry:
# only `methodDef` registrations (empty key) rebuild dispatch
# buckets; the `addMethodToGeneric` flavor (typeKey key) announces
# the uninstantiated generic method, which must never enter a
# bucket (methodsPerGenericType replay is still a todo).
# Under `nim nifc` the replay is deferred: building a bucket forces
# the method's body, and a body loaded mid `loadModuleDependencies`
# registers modules it references in a different path context than
# the lazy loads during codegen do (`flushMethodReplays`).
if x.key.len == 0:
if g.config.cmd == cmdNifC:
g.pendingMethodReplays.add x.sym
else:
registerLoadedMethod(g, x.sym)
else:
discard
proc loadTransitiveHooks(g: ModuleGraph; deps: seq[ModuleSuffix]) =
## Registers the serialized hooks (and enum-to-string procs) of every module
## in the import closure of `deps`. Deliberately does NOT use
## `moduleFromNifFile`: that would register the dep as a fully loaded module
## and a later direct import of it would then skip `replayStateChanges`.
var stack = deps
var interf = initStrTable()
var interfHidden = initStrTable()
while stack.len > 0:
let suffix = stack.pop()
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, {})
registerLoadedHooks(g, precomp.logOps)
# Collect the dependency's macro-cache replay actions (put/inc/add/incl)
# so the importer being compiled also sees macrocache state registered
# by a transitively-imported module. Pragma replay actions are a backend
# concern and are intentionally not collected here.
for n in precomp.topLevel:
if n.kind == nkReplayAction and n.len >= 1 and n[0].kind == nkStrLit and
n[0].strVal in ["put", "inc", "add", "incl"]:
g.transitiveReplayActions.add n
for d in precomp.deps: stack.add d
proc materializeReexportedModule(g: ModuleGraph; mname, msuffix: string): PSym =
## A re-exported MODULE (`import x; export x`) acts as a qualifier in the
## re-exporting module's interface (`asmm.x86.nd`). Reconstruct a module
## symbol for it and make its interface tables available for qualified
## lookup (`someSym` reads `g.ifaces[position]`) — WITHOUT registering
## the module: `Iface.module` stays nil so a later direct import still
## takes the full load path (replayStateChanges etc.).
var isKnown = false
let fIdx = g.config.registerNifSuffix(msuffix, isKnown)
if fIdx.int >= g.ifaces.len: setLen(g.ifaces, fIdx.int + 1)
if g.ifaces[fIdx.int].module != nil and
g.ifaces[fIdx.int].module.name.s == mname:
# properly registered already (directly imported earlier): reuse it
return g.ifaces[fIdx.int].module
result = PSym(kindImpl: skModule, itemId: itemId(int32(fIdx), 0'i32),
name: getIdent(g.cache, mname),
infoImpl: newLineInfo(fIdx, 1, 1),
positionImpl: int(fIdx))
setOwner(result, getPackage(g.config, g.cache, fIdx))
if g.ifaces[fIdx.int].module == nil and
not g.icQualIfaces.containsOrIncl(fIdx.int):
var interf = initStrTable()
var interfHidden = initStrTable()
let precomp = loadNifModule(ast.program, ModuleSuffix(msuffix),
interf, interfHidden, {})
# chains: the re-exported module may itself re-export modules
for (n2, s2) in precomp.reexportedModules:
let inner = materializeReexportedModule(g, n2, s2)
if inner != nil:
strTableAdd(interf, inner)
g.ifaces[fIdx.int].interf = interf
g.ifaces[fIdx.int].interfHidden = interfHidden
proc moduleFromNifFile*(g: ModuleGraph; fileIdx: FileIndex;
flags: set[LoadFlag] = {}): PrecompiledModule =
## Returns 'nil' if the module needs to be recompiled.
@@ -671,7 +1005,7 @@ when not defined(nimKochBootstrap):
let m = PSym(
kindImpl: skModule,
itemId: ItemId(module: int32(fileIdx), item: 0'i32),
itemId: itemId(int32(fileIdx), 0'i32),
name: getIdent(g.cache, splitFile(filename).name),
infoImpl: newLineInfo(fileIdx, 1, 1),
positionImpl: int(fileIdx))
@@ -683,25 +1017,51 @@ when not defined(nimKochBootstrap):
g.ifaces[fileIdx.int].interf,
g.ifaces[fileIdx.int].interfHidden, flags)
result.module = m
for (mname, msuffix) in result.reexportedModules:
let ms = materializeReexportedModule(g, mname, msuffix)
if ms != nil:
strTableAdd(g.ifaces[fileIdx.int].interf, ms)
# Rebuild `procInstCache` from this module's generic-instance OFFERS so a
# consumer's `genericCacheGet` finds the instance and SKIPS re-running
# `instantiateBody` in its own module scope (which lacks symbols visible only
# at the generic's definition site — see ast2nif's `(offer …)`).
for off in result.genericOffers:
g.procInstCache.mgetOrPut(off.generic.itemId, @[]).add PInstantiation(
sym: off.inst, concreteTypes: off.concreteTypes,
genericParamsCount: off.genericParamsCount, compilesId: 0)
# Mark module as cached
g.cachedMods.incl fileIdx.int
g.hookClosure.incl fileIdx.int
# Register hooks from NIF index with the module graph
registerLoadedHooks(g, result.logOps)
for x in result.logOps:
case x.kind
of HookEntry:
g.loadedOps[x.op][x.key] = x.sym
of ConverterEntry:
g.ifaces[fileIdx.int].converters.add x.sym
of PureEnumEntry:
# rebuild the pure-enum list (source path: `addPureEnum`) so importers can
# offer this loaded `{.pure.}` enum's fields as the restricted pure-enum
# fallback (`importPureEnumFields`).
g.ifaces[fileIdx.int].pureEnums.add x.sym
of MethodEntry:
discard "todo"
of EnumToStrEntry:
g.loadedEnumToStringProcs[x.key] = x.sym
discard "dispatch buckets already rebuilt by registerLoadedHooks"
of GenericInstEntry:
raiseAssert "GenericInstEntry should not be in the NIF index"
of HookEntry, EnumToStrEntry:
discard "already done by registerLoadedHooks"
# Register methods per type from NIF index
discard "todo"
# `nim m` loads only its *direct* imports through this proc, but a hook for
# a structural type (e.g. `=destroy` for `seq[PNode]`) lives in the NIF of
# whichever module first lifted it — possibly a dependency of a dependency
# that the current module never imports directly. Walk the whole import
# closure so every serialized hook is visible. (Codegen, `nim nifc`, already
# walks the closure in nifbackend.loadModuleDependencies.)
if g.config.cmd == cmdM:
loadTransitiveHooks(g, result.deps)
proc configComplete*(g: ModuleGraph) =
#rememberStartupConfig(g.startupPackedConfig, g.config)
@@ -730,7 +1090,16 @@ proc getPackage*(graph: ModuleGraph; fileIdx: FileIndex): PSym =
proc belongsToStdlib*(graph: ModuleGraph, sym: PSym): bool =
## Check if symbol belongs to the 'stdlib' package.
sym.getPackageSymbol.getPackageId == graph.systemModule.getPackageId
# Compare the package *name* (an interned ident), not the package symbol's
# `.id`. Under per-module IC (`nim m`) the system module is loaded from a NIF
# in a process that does not compile it from source, so its package symbol is
# reconstructed with a fresh `.id` that no longer matches the freshly-interned
# package of a stdlib module compiled standalone here — making the old id
# comparison wrongly report `false` and inject `--import`ed modules into the
# stdlib. Both are canonically named `stdlib` (lib/stdlib.nimble); in a normal
# `nim c` build (system compiled from source) the ids match too, so this is a
# no-op there.
sym.getPackageSymbol.name.id == graph.systemModule.getPackageSymbol.name.id
proc fileSymbols*(graph: ModuleGraph, fileIdx: FileIndex): SuggestFileSymbolDatabase =
result = graph.suggestSymbols.getOrDefault(fileIdx, newSuggestFileSymbolDatabase(fileIdx, optIdeExceptionInlayHints in graph.config.globalOptions))

View File

@@ -32,7 +32,7 @@ proc newModule*(graph: ModuleGraph; fileIdx: FileIndex): PSym =
let filename = AbsoluteFile toFullPath(graph.config, fileIdx)
# We cannot call ``newSym`` here, because we have to circumvent the ID
# mechanism, which we do in order to assign each module a persistent ID.
result = PSym(kindImpl: skModule, itemId: ItemId(module: int32(fileIdx), item: 0'i32),
result = PSym(kindImpl: skModule, itemId: itemId(int32(fileIdx), 0'i32),
name: getModuleIdent(graph, filename),
infoImpl: newLineInfo(fileIdx, 1, 1))
if not isNimIdentifier(result.name.s):

View File

@@ -125,12 +125,25 @@ proc fileInfoIdx*(conf: ConfigRef; filename: AbsoluteFile): FileIndex =
var dummy: bool = false
result = fileInfoIdx(conf, filename, dummy)
proc expandOrPseudo(filename: string): AbsoluteFile =
# `expandFilename` raises OSError when the path does not exist on disk. That is
# fine for a real source path, but a macro can legitimately set a node's
# line-info file to a name that has no file behind it — e.g. the `???` sentinel
# produced by `toFilename` for a NIF-loaded node whose `fileIndex` is unknown
# (FileIndex(-1)). Falling back to the raw name lets the `AbsoluteFile` overload
# register it as a pseudo-path (like `command line`/`stdin`) instead of crashing
# the whole `nim m` child with an unhandled OSError.
try:
result = AbsoluteFile expandFilename(filename)
except OSError:
result = AbsoluteFile filename
proc fileInfoIdx*(conf: ConfigRef; filename: RelativeFile; isKnownFile: var bool): FileIndex =
fileInfoIdx(conf, AbsoluteFile expandFilename(filename.string), isKnownFile)
fileInfoIdx(conf, expandOrPseudo(filename.string), isKnownFile)
proc fileInfoIdx*(conf: ConfigRef; filename: RelativeFile): FileIndex =
var dummy: bool = false
fileInfoIdx(conf, AbsoluteFile expandFilename(filename.string), dummy)
fileInfoIdx(conf, expandOrPseudo(filename.string), dummy)
proc registerNifSuffix*(conf: ConfigRef; suffix: string; isKnownFile: var bool): FileIndex =
result = conf.m.filenameToIndexTbl.getOrDefault(suffix, InvalidFileIdx)
@@ -511,6 +524,9 @@ proc sourceLine*(conf: ConfigRef; i: TLineInfo): string =
## 1-based index (matches editor line numbers); 1st line is for i.line = 1
## last valid line is `numLines` inclusive
if i.fileIndex.int32 < 0: return ""
# line 0 means "unknown": nodes synthesized from an IC-loaded template or
# macro body carry no source position.
if i.line.int < 1: return ""
let num = numLines(conf, i.fileIndex)
# can happen if the error points to EOF:
if i.line.int > num: return ""

View File

@@ -17,18 +17,39 @@
## 1. Compile modules to NIF: nim m mymodule.nim
## 2. Generate C from NIF: nim nifc myproject.nim
import std/[intsets, tables, sets, os]
import std/[intsets, tables, sets, os, algorithm, syncio, times, strutils]
when defined(nimPreviewSlimSystem):
import std/assertions
import ast, options, lineinfos, modulegraphs, cgendata, cgen,
pathutils, extccomp, msgs, modulepaths, idents, types, ast2nif
pathutils, extccomp, msgs, modulepaths, idents, types, ast2nif, typekeys,
cnif
from cgmeth import generateIfMethodDispatchers
import ic / replayer
proc loadModuleDependencies(g: ModuleGraph; mainFileIdx: FileIndex): seq[PrecompiledModule] =
proc loadModuleDependencies(g: ModuleGraph; mainFileIdx: FileIndex;
nifFiles: var seq[string];
depFlags: set[LoadFlag] = {LoadFullAst}): seq[PrecompiledModule] =
## Traverse the module dependency graph using a stack.
## Returns all modules that need code generation, in dependency order.
##
## The main module is always loaded with its full AST (it is the codegen
## target). `depFlags` governs the rest: the whole-program backend needs every
## module's full AST (it generates code for all of them), but a per-module
## stage codegens only one target, so it loads the others interface-only
## (`depFlags = {}`) — the interface, hooks, methods and the `(replay ...)`
## directives are loaded regardless of `LoadFullAst`, and demanded bodies are
## fetched lazily from the kept-open stream, so the per-module proc-body ASTs
## (the bulk of the memory) are never materialized for non-targets.
# The main module is loaded by its SOURCE FileIndex, but its serialized
# symbols carry the module's NIF suffix. Pre-alias the suffix to the source
# index so that `registerNifSuffix` does not allocate a second FileIndex for
# the same module, which would split its codegen across two C translation
# units (top-level globals in one, procs in the other → undeclared symbols).
g.config.m.filenameToIndexTbl[cachedModuleSuffix(g.config, mainFileIdx)] = mainFileIdx
let mainModule = moduleFromNifFile(g, mainFileIdx, {LoadFullAst})
nifFiles.add toNifFilename(g.config, mainFileIdx)
var stack: seq[ModuleSuffix] = @[]
result = @[]
@@ -46,9 +67,10 @@ proc loadModuleDependencies(g: ModuleGraph; mainFileIdx: FileIndex): seq[Precomp
if not visited.containsOrIncl(suffix.string):
var isKnownFile = false
let fileIdx = g.config.registerNifSuffix(suffix.string, isKnownFile)
let precomp = moduleFromNifFile(g, fileIdx, {LoadFullAst})
let precomp = moduleFromNifFile(g, fileIdx, depFlags)
if precomp.module != nil:
result.add precomp
nifFiles.add toNifFilename(g.config, fileIdx)
for dep in precomp.deps:
if not visited.contains(dep.string):
stack.add dep
@@ -62,7 +84,13 @@ proc setupNifBackendModule(g: ModuleGraph; module: PSym): BModule =
## Set up a BModule for code generation from a NIF module.
if g.backend == nil:
g.backend = cgendata.newModuleList(g)
result = cgen.newModule(BModuleList(g.backend), module, g.config, idGeneratorFromModule(module))
result = cgen.newModule(BModuleList(g.backend), module, g.config, idGeneratorForBackend(module))
proc isMetaIter(t: PType, closure: RootRef): bool =
# openArray/varargs hooks are sem bookkeeping: no real flow ever demands
# them, and generating one pollutes the TU's type cache with a struct
# descriptor for what must remain a (ptr, len) parameter expansion
t.kind in tyMetaTypes + {tyTyped, tyUntyped, tyNone, tyVarargs, tyOpenArray}
proc finishModule(g: ModuleGraph; bmod: BModule) =
# Finalize the module (this adds it to modulesClosed)
@@ -70,9 +98,52 @@ proc finishModule(g: ModuleGraph; bmod: BModule) =
let initStmt = newNode(nkStmtList)
finalCodegenActions(g, bmod, initStmt)
# Generate dispatcher methods
# NB: the method dispatchers are emitted in `emitMethodDispatchers`,
# between the module loop and this finish loop: their bodies demand the
# method definitions, which can in turn demand definitions from modules
# the backend never loaded — and a TU demand-created during the LAST
# finishModule call would miss `modulesClosed` and never be written.
proc emitMethodDispatchers(g: ModuleGraph) =
## Synthesizes the method dispatcher bodies from the replayed dispatch
## buckets (`registerLoadedMethod`) and emits their definitions into the
## main TU. Main is regenerated on every run, so a dispatcher — whose
## body enumerates the whole program's method set — can never go stale
## inside a cached TU; cross-TU callers prototype it (see genProcLvl3).
let bl = BModuleList(g.backend)
var mainMod: BModule = nil
for m in bl.mods:
if m != nil and m.module != nil and sfMainModule in m.module.flags:
mainMod = m
break
if mainMod == nil: return
generateIfMethodDispatchers(g, mainMod.idgen)
for disp in getDispatchers(g):
genProcLvl3(bmod, disp)
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 in {tyTyped, tyUntyped, tyTypeDesc, tyStatic, tyGenericParam,
tyAnything, tyFromExpr, tyError}:
return true
for k in t.kids:
if signatureHasMetaType(k, depth + 1): return true
proc generateCodeForModule(g: ModuleGraph; precomp: PrecompiledModule) =
## Generate C code for a single module.
@@ -81,76 +152,366 @@ proc generateCodeForModule(g: ModuleGraph; precomp: PrecompiledModule) =
if bmod == nil:
bmod = setupNifBackendModule(g, precomp.module)
# Apply the module's recorded C compile/link directives (passl/passc/...)
# before generating code: the link step needs them (e.g. math's -lm).
replayBackendActions(g, precomp.module, precomp.topLevel)
# Generate code for the module's top-level statements
if precomp.topLevel != nil:
cgen.genTopLevelStmt(bmod, precomp.topLevel)
proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) =
## Main entry point for NIF-based C code generation.
## Traverses the module dependency graph and generates C code.
# Per-module backend: emit the bodies of the routines this module OWNS, not
# only the ones its top-level happens to demand. Procs are serialized as lazy
# `(sd ...)` defs (never as `nkProcDef` statements), so `genTopLevelStmt` never
# reaches them; a routine called only from *other* modules would otherwise be
# emitted by nobody, because every module now merely prototypes its foreign
# callees instead of funnelling their bodies (see `cgen.emitsBodyInThisModule`).
# The merge stage's DCE drops whatever turns out globally dead.
if g.config.cmd == cmdNifC and g.config.icBackendStage == "cg":
let modPos = precomp.module.position
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
if s.itemId.module == modPos and
s.kind in {skProc, skFunc, skConverter, skMethod} and
# Only MODULE-level routines: a nested/closure proc (its owner is a
# proc) captures its enclosing scope and cannot be emitted standalone —
# the captured params have no loc → `expr: param not init`. Nested procs
# are emitted via their enclosing routine's lambda-lifting, so seeding
# the enclosing (module-level) routine already covers them.
s.skipGenericOwner != nil and s.skipGenericOwner.kind == skModule and
s.magic == mNone and
# Skip generic instances: they have no single owning-module top-level
# and are emitted by demand (emit-everywhere, deduped by the merge
# stage). An instance has an empty `genericParamsPos` just like a plain
# concrete proc, so only `sfFromGeneric` tells them apart; seeding one
# would force standalone codegen of an instance body whose `when T is X`
# branches were never folded for this path → `genMagicExpr: mIs`.
sfFromGeneric notin s.flags and
# Every other routine the module owns must be emitted here, exported or
# not: a non-exported helper is still reached from another module when a
# `template`/inline routine expands at a call site there (e.g. msgs'
# `internalErrorImpl` behind the `internalError` template), and that
# caller now only prototypes it. `{.error.}`/`compileTime` sentinels and
# bodyless forward decls are not real codegen targets.
{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 and
s.ast[bodyPos].kind != nkEmpty:
# a concrete, non-generic, runtime routine with a real body, owned here
requestProcDef(bmod, s)
# Reset backend state
proc loadBackendModules(g: ModuleGraph; mainFileIdx: FileIndex):
tuple[modules: seq[PrecompiledModule], precompSys: PrecompiledModule,
nifFiles: seq[string]] =
## Shared by the per-module `cg` and `emit` stages: load system + the main
## module's whole import closure and set up a `BModule` for each, so every
## type/symbol resolves and `getCFile` yields the same path both stages use.
## The main module is loaded by its source index (its NIF suffix is aliased to
## it in `loadModuleDependencies`), so it gets exactly one `BModule`.
##
## Only the main module — the codegen target of the stages that use this — is
## loaded with its full AST; every other module is loaded interface-only so
## the whole program's proc bodies are not materialized into this process (that
## was ~1.8 GB for the compiler's main `cg`). The `link` stage codegens nothing
## and only needs each module's `(replay ...)` directives, which load anyway.
resetForBackend(g)
var isKnownFile = false
let systemFileIdx = registerNifSuffix(g.config, "sysma2dyk", isKnownFile)
g.config.m.systemFileIdx = systemFileIdx
#msgs.fileInfoIdx(g.config,
# g.config.libpath / RelativeFile"system.nim")
var precompSys = moduleFromNifFile(g, systemFileIdx, {AlwaysLoadInterface})
g.systemModule = precompSys.module
var nifFiles: seq[string] = @[toNifFilename(g.config, systemFileIdx)]
var modules = loadModuleDependencies(g, mainFileIdx, nifFiles, depFlags = {})
# loadModuleDependencies traverses the project's import closure and stops at
# system. The whole-program backend then demand-loads system's own closure
# (locks, allocators, threads, …) during codegen; the per-module backend
# instead makes every one of those a first-class cg/emit target, so load that
# closure here too — otherwise `findTargetModule` cannot resolve their suffix.
block:
var visited = initHashSet[string]()
visited.incl "sysma2dyk"
for m in modules:
visited.incl cachedModuleSuffix(g.config, FileIndex m.module.position)
var stack: seq[ModuleSuffix] = @[]
if precompSys.module != nil:
for dep in precompSys.deps: stack.add dep
while stack.len > 0:
let suffix = stack.pop()
if not visited.containsOrIncl(suffix.string):
var isKnown = false
let fileIdx = registerNifSuffix(g.config, suffix.string, isKnown)
let precomp = moduleFromNifFile(g, fileIdx, {})
if precomp.module != nil:
modules.add precomp
nifFiles.add toNifFilename(g.config, fileIdx)
for dep in precomp.deps: stack.add dep
flushMethodReplays(g)
for m in modules:
discard setupNifBackendModule(g, m.module)
if precompSys.module != nil:
discard setupNifBackendModule(g, precompSys.module)
result = (modules, precompSys, nifFiles)
# Load system module first - it's always needed and contains essential hooks
var precompSys = PrecompiledModule(module: nil)
precompSys = moduleFromNifFile(g, systemFileIdx, {LoadFullAst, AlwaysLoadInterface})
proc loadDepClosure(g: ModuleGraph; targetSuffix: 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.
##
## 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).
resetForBackend(g)
var isKnownFile = false
let systemFileIdx = registerNifSuffix(g.config, "sysma2dyk", isKnownFile)
g.config.m.systemFileIdx = systemFileIdx
let precompSys = moduleFromNifFile(g, systemFileIdx, {AlwaysLoadInterface})
g.systemModule = precompSys.module
# Load all modules in dependency order using stack traversal
# This must happen BEFORE any code generation so that hooks are loaded into loadedOps
let modules = loadModuleDependencies(g, mainFileIdx)
var modules: seq[PrecompiledModule] = @[]
var visited = initHashSet[string]()
visited.incl "sysma2dyk"
# 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
var stack: seq[ModuleSuffix] = @[]
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:
let suffix = stack.pop()
if not visited.containsOrIncl(suffix.string):
var isKnown2 = false
let fileIdx = registerNifSuffix(g.config, suffix.string, isKnown2)
let precomp = moduleFromNifFile(g, fileIdx, {})
if precomp.module != nil:
modules.add precomp
for dep in precomp.deps: stack.add dep
flushMethodReplays(g)
for m in modules:
discard setupNifBackendModule(g, m.module)
if precompSys.module != nil:
discard setupNifBackendModule(g, precompSys.module)
result = (modules, precompSys, target)
proc findTargetModule(g: ModuleGraph; modules: seq[PrecompiledModule];
precompSys: PrecompiledModule; suffix: string): PrecompiledModule =
## The loaded module whose NIF suffix is `suffix` (the `--icBackendModule`
## value), or a nil module if none matches.
result = PrecompiledModule(module: nil)
for m in modules:
if cachedModuleSuffix(g.config, FileIndex m.module.position) == suffix:
return m
if precompSys.module != nil and
cachedModuleSuffix(g.config, FileIndex precompSys.module.position) == suffix:
return precompSys
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).
##
## `findPendingModule` routes every demand into the target (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
## module still loads everything (`loadBackendModules`) because NimMain's init
## list and the method dispatchers are whole-program; its `cg` runs essentially
## alone (every other `.c.nif` precedes it), so it does not contend for memory.
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
let targetIsMain = g.config.icBackendModule.len == 0 or
g.config.icBackendModule == mainSuffix
var modules: seq[PrecompiledModule]
var precompSys: PrecompiledModule
var target: PrecompiledModule
if targetIsMain:
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
# No whole-program DCE here: each module emits the routines it owns and the
# 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)
else:
# No whole-program load, hence no whole-program DCE: the target 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
generateCodeForModule(g, target)
let bl = BModuleList(g.backend)
# The main module also owns the whole-program method dispatchers + NimMain.
if sfMainModule in target.module.flags:
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
# `cg` processes, so the calls are registered here from each `.c.nif` meta
# head — which is why the main module's `cg` runs last, after every other
# `.c.nif` exists. Modules without init code (no `.c.nif`) register nothing.
for m in bl.mods:
if m != nil and sfMainModule notin m.module.flags:
let heads = readCnifHeads(getCFile(m).string & ".nif")
registerReusedModuleToMain(bl, m, heads.initRequired, heads.datInitRequired)
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))
proc generateMergeStage(g: ModuleGraph) =
## Per-module backend merge (`--icBackendStage:merge`): a pure artifact
## operation, no module graph loaded. Reads every `.c.nif` the `cg` stages
## wrote, computes the global live set and — for each `'u'`-flagged unique
## definition that several `cg` processes emitted (emit-everywhere) — the one
## artifact allowed to embed its body, and writes the decision the `emit`
## stages consume — the cross-process replacement for what used to be
## 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
sort files
let decision = computeMergeDecision(files)
if decision.broken:
rawMessage(g.config, errGenerated,
"per-module backend merge: a .c.nif artifact is missing or unparsable")
return
writeMergeDecision(nimcache / MergeDecisionFile, decision)
if isDefined(g.config, "icDceCheck"):
stderr.writeLine "[icMerge] artifacts: " & $files.len &
" live: " & $decision.live.len & " defs: " & $decision.defs &
" liveDefs: " & $decision.liveDefs & " owned: " & $decision.owners.len
proc generateEmitStage(g: ModuleGraph; mainFileIdx: FileIndex) =
## Per-module backend emit (`--icBackendStage:emit --icBackendModule:<suffix>`):
## 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
var modules: seq[PrecompiledModule]
var precompSys: PrecompiledModule
var target: PrecompiledModule
if targetIsMain:
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)
else:
(modules, precompSys, target) = loadDepClosure(g, g.config.icBackendModule)
if target.module == nil:
rawMessage(g.config, errGenerated,
"per-module emit: module not found for suffix: " & g.config.icBackendModule)
return
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 bmod = BModuleList(g.backend).mods[target.module.position]
let cfile = getCFile(bmod).string
let artifact = cfile & ".nif"
var dropped = 0
let code = renderCFromArtifact(artifact, decision, extractFilename(artifact), dropped)
writeFile(cfile, code)
if isDefined(g.config, "icDceCheck"):
stderr.writeLine "[icEmit] " & extractFilename(cfile) & " dropped " &
$dropped & " bodies (" & $code.len & " bytes)"
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
# Set up backend modules for all modules that need code generation
# 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:
discard setupNifBackendModule(g, m.module)
# Also ensure system module is set up and generated first if it exists
replayBackendActions(g, m.module, m.topLevel)
if precompSys.module != nil:
discard setupNifBackendModule(g, precompSys.module)
generateCodeForModule(g, precompSys)
# Track which modules have been processed to avoid duplicates
var processed = initIntSet()
if precompSys.module != nil:
processed.incl precompSys.module.position
# Generate code for all modules (skip system since it's already processed)
for m in modules:
if not processed.containsOrIncl(m.module.position):
generateCodeForModule(g, m)
# during code generation of `main.nim` we can trigger the code generation
# of symbols in different modules so we need to finish these modules
# here later, after the above loop!
# Important: The main module must be finished LAST so that all other modules
# have registered their init procs before genMainProc uses them.
var mainModule: BModule = nil
for m in BModuleList(g.backend).mods:
replayBackendActions(g, precompSys.module, precompSys.topLevel)
let bl = BModuleList(g.backend)
for m in bl.mods:
if m != nil:
assert m.module != nil
if sfMainModule in m.module.flags:
mainModule = m
else:
finishModule g, m
if mainModule != nil:
finishModule g, mainModule
# Write C files
cgenWriteModules(g.backend, g.config)
# Run C compiler
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
var cf = Cfile(nimname: m.module.name.s, cname: cfile,
obj: completeCfilePath(g.config, toObjFile(g.config, cfile)),
flags: {})
addFileToCompile(g.config, cf)
if g.config.cmd != cmdTcc:
extccomp.callCCompiler(g.config)
if not g.config.hcrOn:
extccomp.writeJsonBuildInstructions(g.config, g.cachedFiles)
proc generateCode*(g: ModuleGraph; mainFileIdx: FileIndex) =
## Main entry point for NIF-based C code generation.
## Traverses the module dependency graph and generates C code.
if g.config.icBackendStage == "cg":
generateCgStage(g, mainFileIdx)
return
elif g.config.icBackendStage == "merge":
generateMergeStage(g)
return
elif g.config.icBackendStage == "emit":
generateEmitStage(g, mainFileIdx)
return
elif g.config.icBackendStage == "link":
generateLinkStage(g, mainFileIdx)
return
else:
rawMessage(g.config, errGenerated,
"the per-module NIF backend requires --icBackendStage:cg|merge|emit|link")

View File

@@ -183,12 +183,6 @@ func `<`*(a: ExprIndex, b: ExprIndex): bool =
func `<=`*(a: ExprIndex, b: ExprIndex): bool =
a.int16 <= b.int16
func `>`*(a: ExprIndex, b: ExprIndex): bool =
a.int16 > b.int16
func `>=`*(a: ExprIndex, b: ExprIndex): bool =
a.int16 >= b.int16
func `==`*(a: ExprIndex, b: ExprIndex): bool =
a.int16 == b.int16

View File

@@ -28,10 +28,13 @@ import
commands, options, msgs, extccomp, main, idents, lineinfos, cmdlinehelper,
pathutils, modulegraphs
from ast2nif import registerNifAstTags
from icconfig import ensureIcConfig
from std/browsers import openDefaultBrowser
from nodejs import findNodeJs
when hasTinyCBackend:
when defined(tinyc): # == hasTinyCBackend; spelled out for the IC dep scanner
import tccgen
when defined(profiler) or defined(memProfiler):
@@ -96,6 +99,11 @@ proc getNimRunExe(conf: ConfigRef): string =
result = ""
proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
# NIF tag registration must not depend on module init order — the IC-built
# compiler orders module init calls differently and the top-level
# `registerTag` initializers then ran against a not-yet-initialized pool,
# corrupting every written NIF (see registerNifAstTags).
registerNifAstTags()
let self = NimProg(
supportsStdinFile: true,
processCmdLine: processCmdLine
@@ -107,6 +115,14 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
self.processCmdLineAndProjectPath(conf)
# `nim ic` driver: ensure the precompiled config exists (produced by a separate
# `nim icconfig` process, skipped when nothing changed) BEFORE config loading,
# 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 == cmdIc:
ensureIcConfig(conf)
var graph = newModuleGraph(cache, conf)
if not self.loadConfigsAndProcessCmdLine(cache, conf, graph):
return

View File

@@ -11,7 +11,7 @@
import
llstream, commands, msgs, lexer, ast,
options, idents, wordrecg, lineinfos, pathutils, scriptconfig
options, idents, wordrecg, lineinfos, pathutils, scriptconfig, icconfig
import std/[os, strutils, strtabs]
@@ -246,6 +246,16 @@ proc getSystemConfigPath*(conf: ConfigRef; filename: RelativeFile): AbsoluteFile
proc loadConfigs*(cfg: RelativeFile; cache: IdentCache; conf: ConfigRef; idgen: IdGenerator) =
setDefaultLibpath(conf)
# The `nim ic` driver and its `nim m`/`nim nifc` children replay the precompiled
# config (produced once by a separate `nim icconfig` process — see
# `icconfig.ensureIcConfig`, which sets `icPreparsedConfig` for the driver
# before this runs; the children get it as a forwarded `--icPreparsedConfig`
# argument) instead of re-reading the `nim.cfg` chain and re-running
# `config.nims` in the VM. A missing/format-incompatible artifact returns false:
# fall through to a normal parse (this is also the path the `nim icconfig`
# producer itself takes, since it runs with no `icPreparsedConfig`).
if conf.icPreparsedConfig.len > 0 and applyIcConfig(conf, conf.icPreparsedConfig):
return
template readConfigFile(path) =
let configPath = path
conf.currentConfigDir = configPath.splitFile.dir.string

View File

@@ -29,6 +29,32 @@ const
nimEnableCovariance* = defined(nimEnableCovariance)
icFormatVersion* = "6"
## 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`
## stamp differs, instead of letting a newer reader mis-parse records
## written by an older compiler (nifmake's rebuild check is mtime-only
## and knows nothing about format changes).
## v2: iface cookie hashes routine SIGNATURES only (no inline-semantics
## body folding); body access now records a NeedsImpl edge instead. A v1
## cache mixes body-sensitive and body-insensitive cookies, so it must be
## wiped rather than warm-rebuilt.
## v3: added the `.s.deps` sidecar (real post-sem imports) and switched the
## macro-generated-import discovery from `icmissing.txt` to it.
## v4: backend C-name scheme change — the module suffix is now the trailing
## token (`name_u<disamb>__<suffix>`, was `name__<suffix>_u<disamb>`), so
## cached `.c.nif` artifacts hold incompatible names and must be wiped.
## v5: data definitions (consts, RTTI) are now wrapped in droppable `'d'`
## cdef directives with an always-present extern declaration, so the
## per-module merge stage can assign them a single owner; old `.c.nif`
## artifacts lack the wrappers.
## v6: `signatureHash`/`hashType` of a builtin type class (`object`, `tuple`,
## `proc`, ...) no longer mixes in the placeholder son's process-local type
## 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.
type # please make sure we have under 32 options
# (improves code efficiency a lot!)
TOption* = enum # **keep binary compatible**
@@ -179,6 +205,7 @@ type
cmdCompileToNif
cmdNifC # generate C code from NIF files
cmdIc # generate .build.nif for nifmake
cmdIcConfig # `nim ic`'s precompiled-config producer (writes ic_config.cfg.nif)
const
cmdBackends* = {cmdCompileToC, cmdCompileToCpp, cmdCompileToOC,
@@ -262,6 +289,11 @@ type
procParamTypeBackendAliases
## Keep the old proc type compatibility rules that ignore backend
## c type aliases.
injectedSymbolRedefinition
## Allow a template to inject a symbol *definition* that is then emitted
## more than once (e.g. a `typed` argument captured by a `{.dirty.}`
## template and re-emitted). This is a redefinition and rejected by
## default; enabling this restores the old, unsound behavior. See #25693.
SymbolFilesOption* = enum
disabledSf, writeOnlySf, readOnlySf, v2Sf, stressTest
@@ -380,6 +412,48 @@ type
lastCmdTime*: float # when caas is enabled, we measure each command
symbolFiles*: SymbolFilesOption
ic*: bool # whether ic is enabled
icGroup*: HashSet[string] # under `nim m`: absolute paths of the modules in
# this strongly-connected import group. They are all
# compiled from source in one process (so mutual
# recursion resolves in-memory) and each gets its NIF
# written, instead of being loaded from a precompiled
# NIF. See `compiler/deps.nim` (SCC grouping).
icProject*: string # under `nim m`/`nim nifc`: absolute path of the
# ORIGINAL project file. The child's own project file
# is the module being compiled, which would make that
# module's package the "main package" and unfilter
# foreign-package diagnostics; the real project
# restores whole-program filtering semantics.
icPreparsedConfig*: string # under the `nim ic` driver and its `nim m`/`nim nifc`
# children: path of the precompiled config artifact.
# When set, `loadConfigs` replays the recorded
# config-file switches from it instead of re-reading
# the `nim.cfg` chain and re-running `config.nims`
# (which the VM makes expensive) per process. The
# artifact itself is produced by a separate
# `nim icconfig` process (see `cmdIcConfig`).
icConfigOut*: string # under `nim icconfig`: the path to write the
# precompiled config artifact to (set via `--o`).
icConfigSwitches*: seq[tuple[switch, arg: string]]
# the config-file (`passPP`) switches applied while
# loading config, in order. Recorded by every nim
# process; only the `ic` driver serialises them.
# Path-search switches are excluded — the driver
# forwards the resolved `searchPaths` as `--path`.
icBackendStage*: string # under `nim nifc`: which stage of the per-module
# backend this invocation runs — "cg" (codegen one
# module to its `.c.nif`), "merge" (global liveness
# + owner assignment across all `.c.nif`), "emit"
# (render one module's `.c` from its `.c.nif` + the
# merge decision), "link" (cc + link every emitted
# `.c`). Empty = whole-program backend (load all,
# 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.
spellSuggestMax*: int # max number of spelling suggestions for typos
cppDefines*: HashSet[string] # (*)
@@ -426,6 +500,12 @@ type
lastMsgWasDot*: set[StdOrrKind] # the last compiler message was a single '.'
projectMainIdx*: FileIndex # the canonical path id of the main module
projectMainIdx2*: FileIndex # consider merging with projectMainIdx
isMainModule*: bool # `nim m`/IC only: whether the single module being
# semantically checked is the program's real entry point.
# Under IC every module is compiled via `nim m` (which sets
# `sfMainModule` so the module writes its own NIF), so
# `sfMainModule` can no longer answer `isMainModule`. The IC
# build file passes `--isMainModule:on` for the root module.
command*: string # the main command (e.g. cc, check, scan, etc)
commandArgs*: seq[string] # any arguments after the main command
commandLine*: string
@@ -582,6 +662,7 @@ proc newConfigRef*(): ConfigRef =
arcToExpand: newStringTable(modeStyleInsensitive),
m: initMsgConfig(),
cppDefines: initHashSet[string](),
icGroup: initHashSet[string](),
headerFile: "", features: {}, legacyFeatures: {},
configVars: newStringTable(modeStyleInsensitive),
symbols: newStringTable(modeStyleInsensitive),
@@ -657,6 +738,7 @@ proc isDefined*(conf: ConfigRef; symbol: string): bool =
of "x86": result = conf.target.targetCPU == cpuI386
of "itanium": result = conf.target.targetCPU == cpuIa64
of "x8664": result = conf.target.targetCPU == cpuAmd64
of "wasm": result = conf.target.targetCPU in {cpuWasm32, cpuWasm64}
of "posix", "unix":
result = conf.target.targetOS in {osLinux, osMorphos, osSkyos, osIrix, osPalmos,
osQnx, osAtari, osAix,

View File

@@ -54,7 +54,10 @@ import
when not defined(nimCustomAst):
import ast
else:
when defined(nimCustomAst):
# NOTE: explicit negated `when` rather than `else:` — nifler's dep scanner
# guards `when`/`elif` imports with their condition but emits `else:` imports
# unconditionally, which would wrongly schedule this module under `nim ic`.
import plugins / customast
import std/strutils
@@ -2241,14 +2244,17 @@ proc parseTypeClassParam(p: var Parser): PNode =
proc parseTypeClass(p: var Parser): PNode =
#| conceptParam = ('var' | 'out' | 'ptr' | 'ref' | 'static' | 'type')? symbol
#| conceptDecl = 'concept' conceptParam ^* ',' (pragma)? ('of' typeDesc ^* ',')?
#| conceptDecl = 'concept' (conceptParam ^* ',' (pragma)?)? ('of' typeDesc ^* ',')?
#| &IND{>} stmt
result = newNodeP(nkTypeClassTy, p)
getTok(p)
if p.tok.tokType == tkComment:
skipComment(p, result)
if p.tok.indent < 0:
if p.tok.tokType == tkOf and p.tok.indent < 0:
# new-styled `concept of A, B` on the same line as `concept`
result.add(p.emptyNode)
elif p.tok.indent < 0:
var args = newNodeP(nkArgList, p)
result.add(args)
args.add(p.parseTypeClassParam)
@@ -2274,9 +2280,10 @@ proc parseTypeClass(p: var Parser): PNode =
result.add(p.emptyNode)
if p.tok.tokType == tkComment:
skipComment(p, result)
# an initial IND{>} HAS to follow:
# an initial IND{>} HAS to follow, unless this concept inherits requirements:
if not realInd(p):
if result.isNewStyleConcept:
let hasParents = result[2].kind != nkEmpty
if result.isNewStyleConcept and not hasParents:
parMessage(p, "routine expected, but found '$1' (empty new-styled concepts are not allowed)", p.tok)
result.add(p.emptyNode)
else:

View File

@@ -15,7 +15,7 @@ import ../dist/checksums/src/checksums/sha1
when not defined(leanCompiler):
import jsgen, docgen2
import std/[syncio, objectdollar, assertions, tables, strutils, strtabs]
import std/[syncio, objectdollar, assertions, tables, strutils, strtabs, sets, intsets]
import renderer
import ic/replayer
@@ -243,9 +243,14 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
when not defined(nimKochBootstrap):
# For cmdM: only write NIF for the main module, not for imported modules
# (imported modules should be loaded from existing NIF files)
# (imported modules should be loaded from existing NIF files). Members of the
# current strongly-connected import group (`--icGroup`) are the exception:
# they are compiled from source here, so each must write its own NIF.
let shouldWriteNif = (optCompress in graph.config.globalOptions) or
(graph.config.cmd == cmdM and sfMainModule in module.flags)
(graph.config.cmd == cmdM and
(sfMainModule in module.flags or
(graph.config.icGroup.len > 0 and
toFullPath(graph.config, module.position.FileIndex) in graph.config.icGroup)))
if shouldWriteNif and not graph.config.isDefined("nimscript"):
topLevelStmts.add finalNode
# Collect replay actions from both pragma computations and VM state diff
@@ -259,16 +264,99 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
if m == module:
replayActions.add n
writeNifModule(graph.config, module.position.int32, topLevelStmts, graph.opsLog, replayActions)
# NeedsImpl edge recording: which modules' bodies this process consumed
# at compile time (VM/getImpl). For an --icGroup cycle every member gets
# the union; intra-group entries are filtered by the writer.
var implDeps: seq[int] = @[]
for id in graph.icImplDeps: implDeps.add id
# Generic-instance OFFERS: every instance THIS module created, so a
# consumer reuses it rather than re-instantiating in its own scope (which
# cannot see symbols visible only at the generic's definition site — e.g.
# a distinct type's `==`). See ast2nif.writeNifModule / moduleFromNifFile.
var genericOffers: seq[tuple[generic, inst: PSym;
concreteTypes: seq[PType]; genericParamsCount: int]] = @[]
for genItemId, instList in graph.procInstCache:
for inst in instList:
if inst.sym != nil and inst.sym.itemId.module == module.position and
inst.sym.instantiatedFrom != nil and inst.compilesId == 0:
# `concreteTypes` is pre-sized to `paramsLen+gp.len`; a tail slot can
# stay nil (e.g. fewer materialized params than `paramsLen`). Such an
# offer can't be serialized — skip it (the consumer re-instantiates,
# the prior behaviour) rather than emit a nil type reference.
var hasNil = false
for ct in inst.concreteTypes:
if ct == nil: hasNil = true; break
if not hasNil:
genericOffers.add (inst.sym.instantiatedFrom, inst.sym,
inst.concreteTypes, inst.genericParamsCount)
# The module's REAL resolved direct imports (incl. macro/template-generated
# ones with no surviving syntactic node). Passed to writeNifModule so the
# NIF `deps` section is complete (the backend closure walk needs it), and
# reused below for the `.s.deps` sidecar (frontend graph re-derivation).
let resolvedImportDeps = graph.importDeps.getOrDefault(module.position.FileIndex, @[])
writeNifModule(graph.config, module.position.int32, topLevelStmts, graph.opsLog,
replayActions, implDeps, reexportedModuleSyms(graph, module),
genericOffers, resolvedImportDeps)
# 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] = @[]
for f in resolvedImportDeps:
semDepPaths.add toFullPath(graph.config, f)
writeSemDeps(graph.config, module.position.int32, semDepPaths)
result = true
proc loadedDefSym(defs: PNode): PSym =
## The defined symbol of a let/var entry as it loads back from a NIF: the
## section child is a bare `nkSym` (the `(sd …)` reference), but be defensive
## about the from-source shapes too (`nkIdentDefs`, a pragma-wrapped name).
case defs.kind
of nkSym: result = defs.sym
of nkPragmaExpr:
result = if defs.len > 0: loadedDefSym(defs[0]) else: nil
of nkIdentDefs, nkConstDef:
result = if defs.len > 0: loadedDefSym(defs[0]) else: nil
else: result = nil
proc initLoadedCompileTimeGlobals(graph: ModuleGraph; module: PSym; topLevel: PNode) =
## Eagerly initialize the compile-time globals (`let/var {.compileTime.}`) of a
## module restored from a NIF. In a normal sem these VM slots are filled by
## `setupCompileTimeVar` (semstmts) as the section is semchecked; a NIF-loaded
## module is never semchecked, so without this a macro or compile-time proc that
## reads such a global finds a nil slot. The lazy `vmgen.genGlobalInit` fallback
## is order-fragile across proc boundaries (it emits the init at the first
## VM-gen'd reference, which need not be the first one executed), so the init has
## to happen here, once, before any of the module's code can run. The symbol's
## own `ast` is the `nkIdentDefs` (initializer included); re-wrap it in a section
## exactly as semstmts does and hand it to the same evaluator.
if topLevel == nil: return
let idgen = idGeneratorFromModule(module)
for stmt in topLevel:
if stmt.kind notin {nkLetSection, nkVarSection}: continue
for defs in stmt:
let s = loadedDefSym(defs)
if s != nil and s.kind in {skLet, skVar} and
{sfCompileTime, sfGlobal} <= s.flags and
s.ast != nil and s.ast.kind == nkIdentDefs:
var sect = newNodeI(stmt.kind, s.info)
sect.add s.ast
setupCompileTimeVar(module, idgen, graph, sect)
proc compilePipelineModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymFlags; fromModule: PSym = nil): PSym =
var flags = flags
if fileIdx == graph.config.projectMainIdx2: flags.incl sfMainModule
result = graph.getModule(fileIdx)
template processModuleAux(moduleStatus) =
when defined(icDbg):
block:
let dbgf = open("/tmp/defdbg.txt", fmAppend)
dbgf.writeLine toFullPath(graph.config, fileIdx) &
" nimStackTraceOverride=" & $isDefined(graph.config, "nimStackTraceOverride") &
" nimscript=" & $isDefined(graph.config, "nimscript") &
" optCompress=" & $(optCompress in graph.config.globalOptions) &
" cmd=" & $graph.config.cmd
dbgf.close()
onProcessing(graph, fileIdx, moduleStatus, fromModule = fromModule)
var s: PLLStream = nil
if sfMainModule in flags:
@@ -278,14 +366,34 @@ proc compilePipelineModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymF
if result == nil:
when not defined(nimKochBootstrap):
# For cmdM: load imports from NIF files (but compile the main module from source)
# Skip when withinSystem is true (compiling system.nim itself)
# Skip when withinSystem is true (compiling system.nim itself).
# Also skip for members of the current strongly-connected import group
# (`--icGroup`): those are mutually recursive with the main module and have
# no precompiled NIF yet, so they must be compiled from source in this same
# process (falling through below) — that resolves the cycle in-memory, the
# same way the non-incremental compiler handles recursive module imports.
if graph.config.cmd == cmdM and
sfMainModule notin flags and
not graph.withinSystem and
not graph.config.isDefined("nimscript"):
not graph.config.isDefined("nimscript") and
(graph.config.icGroup.len == 0 or
toFullPath(graph.config, fileIdx) notin graph.config.icGroup):
let precomp = moduleFromNifFile(graph, fileIdx)
if precomp.module == nil:
let nifPath = toNifFilename(graph.config, fileIdx)
# Macro-generated imports (e.g. chronicles' parseStmt("import
# chronicles/textlines") driven by the chronicles_sinks define) are
# invisible to the static scanner, so this module's NIF was never
# built. The importer already recorded this import via
# addImportFileDep, so flush every module's `.s.deps`: `nim ic` reads
# it, re-derives the graph with the missing node + edge, and reruns
# the frontend. We still error — this process cannot finish sem
# without the import — but the discovery is structured data now, not
# a side-channel file.
for importer, deps in graph.importDeps.pairs:
var paths: seq[string] = @[]
for f in deps: paths.add toFullPath(graph.config, f)
writeSemDeps(graph.config, importer.int32, paths)
globalError(graph.config, unknownLineInfo,
"nim m requires precompiled NIF for import: " & toFullPath(graph.config, fileIdx) &
" (expected: " & nifPath & ")")
@@ -296,9 +404,33 @@ proc compilePipelineModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymF
if sfSystemModule in flags:
graph.systemModule = result
partialInitModule(result, graph, fileIdx, AbsoluteFile(toFullPath(graph.config, fileIdx)))
# Replay state changes from the loaded NIF module
if result.ast != nil:
replayStateChanges(result, graph)
# Replay the module's recorded state changes: macro-cache operations
# (std/macrocache puts/incs/adds/incls) plus a few pragmas. The loader
# parsed them into `precomp.topLevel` (mixed with other top-level nodes),
# so filter to the replay actions. A loaded module's `ast` is never
# rebuilt, so this used to be skipped (`result.ast == nil`) and a
# NIF-loaded module's macro cache was lost — e.g. nim-serialization's
# flavor registration became invisible to dependents (`DefaultFlavor:
# automatic serialization is not enabled`).
var replayList = newNodeI(nkStmtList, result.info)
for n in precomp.topLevel:
# Only macro-cache ops (put/inc/add/incl). The pragma replay actions
# (compile/link/passc/hint/...) are a backend/link concern handled by
# the nifc closure, and re-emitting a loaded module's hints/warnings on
# every import would be wrong — so they are deliberately skipped here.
if n.kind == nkReplayAction and n.len >= 1 and n[0].kind == nkStrLit and
n[0].strVal in ["put", "inc", "add", "incl"]:
replayList.add n
# Plus the macro-cache actions of the module's transitive import closure
# (collected by the moduleFromNifFile call above via loadTransitiveHooks),
# so a flavor/type registered in an indirectly-imported module is visible.
for n in graph.transitiveReplayActions: replayList.add n
graph.transitiveReplayActions.setLen 0
if replayList.len > 0:
replayStateChanges(result, graph, replayList)
# Fill the VM slots of the module's `{.compileTime.}` globals now (sem
# would have, but a NIF-loaded module is never semchecked).
initLoadedCompileTimeGlobals(graph, result, precomp.topLevel)
return result # Return early, don't process from source
let path = toFullPath(graph.config, fileIdx)
let filename = AbsoluteFile path
@@ -364,7 +496,14 @@ proc compilePipelineProject*(graph: ModuleGraph; projectFileIdx = InvalidFileIdx
let projectFile = if projectFileIdx == InvalidFileIdx: conf.projectMainIdx else: projectFileIdx
conf.projectMainIdx2 = projectFile
let packSym = getPackage(graph, projectFile)
var packSym = getPackage(graph, projectFile)
if graph.config.cmd in {cmdM, cmdNifC} and graph.config.icProject.len > 0:
# per-module IC children: the process' project file is the MODULE being
# compiled, which would make its package the "main package" and unfilter
# foreign-package diagnostics (a vendored package's hintAsError promotion
# then aborts builds the whole-program compilation accepts). Use the
# original project, forwarded by deps.nim via --icproject.
packSym = getPackage(graph, fileInfoIdx(graph.config, AbsoluteFile graph.config.icProject))
graph.config.mainPackageId = packSym.getPackageId
graph.importStack.add projectFile
@@ -375,6 +514,9 @@ proc compilePipelineProject*(graph: ModuleGraph; projectFileIdx = InvalidFileIdx
elif graph.config.cmd == cmdM:
# For cmdM: load system.nim from NIF first, then compile the main module
connectPipelineCallbacks(graph)
# Record the main module so the IC loader won't materialise duplicate stubs
# for its own symbols when a dependency (e.g. system) re-exports them.
setIcMainModule(projectFile)
graph.config.m.systemFileIdx = fileInfoIdx(graph.config,
graph.config.libpath / RelativeFile"system.nim")
when not defined(nimKochBootstrap):

View File

@@ -1,3 +1,4 @@
import std/intsets
import ast, options, lineinfos, pathutils, msgs, modulegraphs, packages
proc skipCodegen*(config: ConfigRef; n: PNode): bool {.inline.} =
@@ -23,4 +24,3 @@ proc prepareConfigNotes*(graph: ModuleGraph; module: PSym) =
proc moduleHasChanged*(graph: ModuleGraph; module: PSym): bool {.inline.} =
result = true
#module.id >= 0 or isDefined(graph.config, "nimBackendAssumesChange")

View File

@@ -211,7 +211,7 @@ type
cpuPowerpc64el, cpuSparc, cpuVm, cpuHppa, cpuIa64, cpuAmd64, cpuMips,
cpuMipsel, cpuArm, cpuArm64, cpuJS, cpuNimVM, cpuAVR, cpuMSP430,
cpuSparc64, cpuS390x, cpuMips64, cpuMips64el, cpuRiscV32, cpuRiscV64,
cpuEsp, cpuWasm32, cpuE2k, cpuLoongArch64
cpuEsp, cpuWasm32, cpuE2k, cpuLoongArch64, cpuWasm64
type
TInfoCPU* = tuple[name: string, intSize: int, endian: Endianness,
@@ -249,7 +249,8 @@ const
(name: "esp", intSize: 32, endian: littleEndian, floatSize: 64, bit: 32),
(name: "wasm32", intSize: 32, endian: littleEndian, floatSize: 64, bit: 32),
(name: "e2k", intSize: 64, endian: littleEndian, floatSize: 64, bit: 64),
(name: "loongarch64", intSize: 64, endian: littleEndian, floatSize: 64, bit: 64)]
(name: "loongarch64", intSize: 64, endian: littleEndian, floatSize: 64, bit: 64),
(name: "wasm64", intSize: 64, endian: littleEndian, floatSize: 64, bit: 64)]
type
Target* = object

View File

@@ -105,9 +105,12 @@ proc fitNode(c: PContext, formal: PType, arg: PNode; info: TLineInfo): PNode =
result.typ = formal
elif arg.kind in nkSymChoices and formal.skipTypes(abstractInst).kind == tyEnum:
# Pick the right 'sym' from the sym choice by looking at 'formal' type:
# The choice candidates may be wrapped in `var`/`lent` when they come from
# a loop-local view, but for enum disambiguation only the underlying enum
# type matters.
result = nil
for ch in arg:
if sameType(ch.typ, formal):
if sameType(ch.typ.skipTypes({tyVar, tyLent}), formal):
return ch
typeMismatch(c.config, info, formal, arg.typ, arg)
else:
@@ -247,6 +250,26 @@ proc newSymG*(kind: TSymKind, n: PNode, c: PContext): PSym =
if result.kind notin {kind, skTemp}:
localError(c.config, n.info, "cannot use symbol of kind '$1' as a '$2'" %
[result.kind.toHumanStr, kind.toHumanStr])
# bug #25693: a local declared inside a template/macro operand (recorded in
# `shadowDiscardedDefs`) can be captured by a `{.dirty.}` template and
# re-emitted as a definition more than once. The first emission keeps the
# original symbol (so a leaked dirty-template name still resolves); every
# later emission gets a fresh copy, so distinct emissions don't share one
# symbol - which the destructor/liveness analysis would otherwise miscompile.
# Unlike a plain redefinition check this is control-flow agnostic, so the
# common "emit a `typed` body in several mutually-exclusive branches" pattern
# keeps working. gensym'ed locals (and ones derived from a gensym name) are
# excluded: the gensym machinery already keeps their names unique, and a
# fresh copy would reuse the unique name and clash in the same scope.
if kind in {skVar, skLet, skForVar} and
{sfGenSym, sfWasGenSym} * result.flags == {} and
result.id in c.shadowDiscardedDefs:
if containsOrIncl(c.realizedDefs, result.id):
let fresh = copySym(result, c.idgen)
fresh.ast = result.ast
put(c.p, result, fresh)
c.hasSymRedefs = true
result = fresh
when false:
if sfGenSym in result.flags and result.kind notin {skTemplate, skMacro, skParam}:
# declarative context, so produce a fresh gensym:

View File

@@ -90,8 +90,14 @@ proc addTypeBoundSymbols(graph: ModuleGraph, arg: PType, name: PIdent,
# argument must be typed first, meaning arguments always
# matching `untyped` are ignored
let t = nominalRoot(arg)
if t != nil and t.owner.kind == skModule:
# search module for routines attachable to `t`
if t != nil and t.owner.kind == skModule and
t.owner.position >= 0 and t.owner.position < graph.ifaces.len:
# search module for routines attachable to `t`.
# Under IC the nominal type may have been loaded from a NIF file, in which
# case its owner module is a stub whose `position` (a NIF-suffix file index)
# has no `ifaces` slot; such type-bound ops are reachable through normal
# imports instead, so skip the direct module scan to avoid an out-of-range
# access.
let module = t.owner
var iter = default(ModuleIter)
var s = initModuleIter(iter, graph, module, name)
@@ -726,6 +732,15 @@ proc indexTypesMatch(c: PContext, f, a: PType, arg: PNode): PNode =
result = paramTypesMatch(m, f, a, arg, nil)
if m.genericConverter and result != nil:
instGenericConvertersArg(c, result, m)
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),
" 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,
" mod=", toFullPath(c.config, a2.itemId.module.FileIndex),
" sym=", (if a2.sym != nil: $a2.sym.itemId else: "nil"), " state=", a2.state
proc inferWithMetatype(c: PContext, formal: PType,
arg: PNode, coerceDistincts = false): PNode =
@@ -968,7 +983,12 @@ proc explicitGenericSym(c: PContext, n: PNode, s: PSym, errors: var CandidateErr
diagnostics: m.diagnostics))
return nil
var newInst = generateInstance(c, s, m.bindings, n.info)
newInst.typ.excl tfUnresolved
# `generateInstance` may return an instance REUSED from another module's NIF
# `(offer …)` — its type is Sealed (immutable). Such an instance is already
# fully resolved (`tfUnresolved` cleared at its original instantiation), so the
# `excl` is a no-op; skip it rather than assert on a Sealed-type mutation.
if newInst.typ.state != Sealed:
newInst.typ.excl tfUnresolved
let info = getCallLineInfo(n)
markUsed(c, info, s, isGenericInstance = false)
onUse(info, s, isGenericInstance = false)

View File

@@ -189,6 +189,18 @@ type
inTypeofContext*: int
semAsgnOpr*: proc (c: PContext; n: PNode; k: TNodeKind): PNode {.nimcall.}
shadowDiscardedDefs*: IntSet
# ids of local symbols that were declared inside a template/macro operand's
# shadow scope and then discarded; re-emitting such a symbol as a
# definition gives a fresh copy so distinct emissions don't share a symbol.
# See bug #25693 and `rememberShadowDefs`.
realizedDefs*: IntSet
# ids from `shadowDiscardedDefs` already realized once; the first emission
# keeps the original symbol (so leaked dirty-template names still resolve),
# later emissions get a fresh copy.
hasSymRedefs*: bool
# set once a redefinition mapping has been installed; makes `getGenSym`
# consult the proc-con mapping for non-gensym symbols too.
TBorrowState* = enum
bsNone, bsReturnNotMatch, bsNoDistinct, bsGeneric, bsNotSupported, bsMatch
@@ -281,7 +293,10 @@ proc get*(p: PProcCon; key: PSym): PSym =
result = p.mapping.getOrDefault(key.itemId)
proc getGenSym*(c: PContext; s: PSym): PSym =
if sfGenSym notin s.flags: return s
# `c.hasSymRedefs` additionally routes ordinary (non-gensym) symbols through
# the mapping so a re-emitted definition can redirect them to its fresh copy,
# see bug #25693 and `newSymG`.
if sfGenSym notin s.flags and not c.hasSymRedefs: return s
var it = c.p
while it != nil:
result = get(it, s)
@@ -343,6 +358,8 @@ proc newContext*(graph: ModuleGraph; module: PSym): PContext =
userPragmas: initStrTable(),
generics: @[],
unknownIdents: initIntSet(),
shadowDiscardedDefs: initIntSet(),
realizedDefs: initIntSet(),
cache: graph.cache,
graph: graph,
signatures: initStrTable(),
@@ -353,7 +370,16 @@ proc addIncludeFileDep*(c: PContext; f: FileIndex) =
discard
proc addImportFileDep*(c: PContext; f: FileIndex) =
discard
# Under `nim m` (the IC frontend) record the REAL direct imports of the
# current module as sem resolves them — including imports a macro generated
# (e.g. chronicles' `parseStmt("import chronicles/textlines")`), which the
# static dependency scanner never sees. `nim ic` writes this set as the
# module's `.s.deps` sidecar and re-derives the build graph from it, so the
# discovery is structured data instead of a build-failure side channel.
if c.config.cmd == cmdM:
let importer = c.module.position.FileIndex
var deps = addr c.graph.importDeps.mgetOrPut(importer, @[])
if f notin deps[]: deps[].add f
proc addPragmaComputation*(c: PContext; n: PNode) =
# Also store for NIF-based IC (cmdM mode or optCompress)
@@ -370,6 +396,18 @@ proc addConverter*(c: PContext, conv: PSym) =
assert conv != nil
if inclSym(c.converters, conv):
add(c.graph.ifaces[c.module.position].converters, conv)
# Record for IC: the loader rebuilds Iface.converters from the NIF's
# (repconverter ...) entries (moduleFromNifFile). This must capture not only
# converters DEFINED in this module (addConverterDef) but also ones IMPORTED
# from another module here (importer.addUnnamedIt re-adds a re-exported
# module's converters via this proc). Otherwise a loaded module's
# re-exported converters were invisible to importers and implicit
# conversions silently stopped matching at a consumer that reaches the
# converter only through this module's re-export chain (e.g. faststreams'
# `InputStreamHandle -> InputStream` via ssz_serialization, breaking
# `SSZ.decode`/`encode`). `inclSym` guards against duplicate log entries.
c.graph.opsLog.add LogEntry(kind: ConverterEntry, module: c.module.position,
key: "", sym: conv)
proc addConverterDef*(c: PContext, conv: PSym) =
addConverter(c, conv)
@@ -377,6 +415,13 @@ proc addConverterDef*(c: PContext, conv: PSym) =
proc addPureEnum*(c: PContext, e: PSym) =
assert e != nil
add(c.graph.ifaces[c.module.position].pureEnums, e)
# record for IC: a NIF-loaded module rebuilds `Iface.pureEnums` from these log
# entries (moduleFromNifFile); without it a loaded module's pure enums were
# invisible to importers, so `importPureEnumFields` never offered their fields
# and unqualified pure-enum values stopped resolving. (Same pattern as
# `addConverterDef`.)
c.graph.opsLog.add LogEntry(kind: PureEnumEntry, module: c.module.position,
key: "", sym: e)
proc addPattern*(c: PContext, p: PSym) =
assert p != nil

View File

@@ -27,6 +27,11 @@ const
proc semTemplateExpr(c: PContext, n: PNode, s: PSym,
flags: TExprFlags = {}; expectedType: PType = nil): PNode =
rememberExpansion(c, n.info, s)
# IC: this expands `s`'s body into the current module's sem, so the module
# depends on that body — record a NeedsImpl (strong) edge to `s`'s module.
# The iface cookie hashes only signatures now, so a template body edit moves
# only the impl cookie, and just the modules that expanded it re-sem.
recordIcImplDep(c.graph, s)
let info = getCallLineInfo(n)
markUsed(c, info, s)
onUse(info, s)
@@ -57,6 +62,16 @@ proc semOperand(c: PContext, n: PNode, flags: TExprFlags = {}): PNode =
elif {efWantStmt, efAllowStmt} * flags != {}:
result.typ = newTypeS(tyVoid, c)
else:
when defined(icDbgRefc):
echo "[icNoType] semOperand: ", renderTree(result, {renderNoComments}),
" kind=", result.kind,
(if result.kind in {nkCall, nkCommand} and result[0].kind == nkSym:
" calleeTyp=" & (if result[0].sym.typ == nil: "NIL" else:
$result[0].sym.typ.kind & " ret=" &
(if result[0].sym.typ.returnType == nil: "NIL"
else: $result[0].sym.typ.returnType.kind))
else: "")
echo getStackTrace()
localError(c.config, n.info, errExprXHasNoType %
renderTree(result, {renderNoComments}))
result.typ = errorType(c)
@@ -83,6 +98,17 @@ proc semExprWithType(c: PContext, n: PNode, flags: TExprFlags = {}, expectedType
if result.typ == nil and efInTypeof in flags:
result.typ = c.voidType
elif result.typ == nil or result.typ == c.enforceVoidContext:
when defined(icDbgRefc):
echo "[icNoType] semExprWithType: ", renderTree(result, {renderNoComments}),
" kind=", result.kind,
(if result.kind in {nkCall, nkCommand} and result[0].kind == nkSym:
" callee=" & result[0].sym.name.s &
" calleeTyp=" & (if result[0].sym.typ == nil: "NIL" else:
$result[0].sym.typ.kind & " ret=" &
(if result[0].sym.typ.returnType == nil: "NIL"
else: $result[0].sym.typ.returnType.kind))
else: "")
echo getStackTrace()
localError(c.config, n.info, errExprXHasNoType %
renderTree(result, {renderNoComments}))
result.typ = errorType(c)
@@ -106,7 +132,9 @@ proc semExprWithType(c: PContext, n: PNode, flags: TExprFlags = {}, expectedType
proc semExprNoDeref(c: PContext, n: PNode, flags: TExprFlags = {}): PNode =
result = semExprCheck(c, n, flags)
if result.typ == nil:
if result.typ == nil and efInTypeof in flags:
result.typ = c.voidType
elif result.typ == nil:
localError(c.config, n.info, errExprXHasNoType %
renderTree(result, {renderNoComments}))
result.typ = errorType(c)
@@ -197,6 +225,29 @@ proc semOpenSym(c: PContext, n: PNode, flags: TExprFlags, expectedType: PType,
# set symchoice node type back to None
n.typ = newTypeS(tyNone, c)
proc resolveOpenSymDotRhs(c: PContext, n: PNode): PNode =
## Resolves an `nkOpenSym` in the field position of a dot expression.
## The dot handling (`builtinFieldAccess`, `dotTransformation`) matches on
## the node kind of the RHS directly, so the wrapper cannot be left for
## `semExpr` to unwrap; without this the captured symbol degrades to a
## plain identifier that is then only looked up in the instantiation
## context. Mirrors `semOpenSym`: a symbol injected during instantiation
## under the current proc replaces the captured symbol, otherwise the
## captured node is used.
let inner = n[0]
result = inner
if inner.kind != nkSym: return
let id = newIdentNode(inner.sym.name, n.info)
c.isAmbiguous = false
let s2 = qualifiedLookUp(c, id, {})
if s2 != nil and not c.isAmbiguous and s2 != inner.sym:
# only consider symbols defined under the current proc:
var o = s2.owner
while o != nil:
if o == c.p.owner:
return id
o = o.owner
proc semSymChoice(c: PContext, n: PNode, flags: TExprFlags = {}, expectedType: PType = nil): PNode =
if n.kind == nkOpenSymChoice:
result = semOpenSym(c, n, flags, expectedType,
@@ -1524,6 +1575,9 @@ proc builtinFieldAccess(c: PContext; n: PNode; flags: var TExprFlags): PNode =
suggestExpr(c, n)
if exactEquals(c.config.m.trackPos, n[1].info): suggestExprNoCheck(c, n)
if n[1].kind == nkOpenSym:
n[1] = resolveOpenSymDotRhs(c, n[1])
var s = qualifiedLookUp(c, n, {checkAmbiguity, checkUndeclared, checkModule})
if s != nil:
if s.kind in OverloadableSyms:
@@ -1851,6 +1905,22 @@ proc takeImplicitAddr(c: PContext, n: PNode; isLent: bool): PNode =
n.typ = n.typ.elementType
result.add(n)
proc markResultVarIsPtr(c: PContext, x: PNode) {.inline.} =
## Set `tfVarIsPtr` on the (result) sym node's type. Under IC that type can be a
## NIF-loaded (Sealed) and interned instance which must not be mutated in place
## (it could corrupt other users of the shared type, and the assert forbids it):
## give this result its own copy carrying the flag, exactly like a from-source
## compile has a fresh result type here.
if tfVarIsPtr in x.typ.flags: return
if x.typ.state == Sealed:
let fresh = copyType(x.typ, c.idgen, x.typ.owner)
fresh.incl tfVarIsPtr
x.typ = fresh
if x.kind == nkSym and x.sym.state != Sealed:
x.sym.typ = fresh
else:
x.typ.incl tfVarIsPtr
proc asgnToResultVar(c: PContext, n, le, ri: PNode) {.inline.} =
if le.kind == nkHiddenDeref:
var x = le[0]
@@ -1858,10 +1928,10 @@ proc asgnToResultVar(c: PContext, n, le, ri: PNode) {.inline.} =
if x.sym.kind == skResult and (x.typ.kind in {tyVar, tyLent} or classifyViewType(x.typ) != noView):
n[0] = x # 'result[]' --> 'result'
n[1] = takeImplicitAddr(c, ri, x.typ.kind == tyLent)
x.typ.incl tfVarIsPtr
markResultVarIsPtr(c, x)
#echo x.info, " setting it for this type ", typeToString(x.typ), " ", n.info
elif sfGlobal in x.sym.flags:
x.typ.incl tfVarIsPtr
markResultVarIsPtr(c, x)
proc borrowCheck(c: PContext, n, le, ri: PNode) =
const
@@ -2118,6 +2188,12 @@ proc semProcBody(c: PContext, n: PNode; expectedType: PType = nil): PNode =
if c.p.owner.kind notin {skMacro, skTemplate} and
c.p.resultSym != nil and c.p.resultSym.typ.isMetaType:
when defined(icDbgRefc):
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,
" state=", c.p.resultSym.typ.state
if isEmptyType(result.typ):
# we inferred a 'void' return type:
c.p.resultSym.typ = errorType(c)

View File

@@ -610,10 +610,21 @@ proc getConstExpr(m: PSym, n: PNode; idgen: IdGenerator; g: ModuleGraph): PNode
var s = n.sym
case s.kind
of skEnumField:
when defined(icDbg):
if n.typ == nil:
echo "ENUMFIELD niltyp sym=", s.name.s, " symtyp=",
(if s.typ == nil: "nil" else: $s.typ.kind), " lazy=", nfLazyType in n.flags,
" symstate=", s.state, " symid=", s.itemId
result = newIntNodeT(toInt128(s.position), n, idgen, g)
of skConst:
case s.magic
of mIsMainModule: result = newIntNodeT(toInt128(ord(sfMainModule in m.flags)), n, idgen, g)
of mIsMainModule:
# Under `nim m` (IC) `sfMainModule` is set on every module that is being
# compiled (so it writes its own NIF), so it cannot answer `isMainModule`;
# the IC build file marks the real entry point with `--isMainModule:on`.
let isMain = if g.config.cmd == cmdM: g.config.isMainModule
else: sfMainModule in m.flags
result = newIntNodeT(toInt128(ord(isMain)), n, idgen, g)
of mCompileDate: result = newStrNodeT(getDateStr(), n, g)
of mCompileTime: result = newStrNodeT(getClockStr(), n, g)
of mCpuEndian: result = newIntNodeT(toInt128(ord(CPU[g.config.target.targetCPU].endian)), n, idgen, g)

View File

@@ -129,7 +129,14 @@ proc semGenericStmtSymbol(c: PContext, n: PNode, s: PSym,
result.typ = nil
onUse(n.info, s)
of skParam:
result = n
if s.owner == c.p.owner:
# Parameters of the routine currently being semchecked stay as local
# identifiers
result = n
else:
# Preserve captured outer parameters so nested generic procs can still
# see them after the generic pre-pass.
result = newSymNode(s, n.info)
onUse(n.info, s)
of skType:
if (s.typ != nil) and

View File

@@ -119,11 +119,44 @@ proc freshGenSyms(c: PContext; n: PNode, owner, orig: PSym, symMap: var SymMappi
proc addParamOrResult(c: PContext, param: PSym, kind: TSymKind)
proc aliasLoadedTypedescParams(c: PContext, instantiated, orig: PSym): bool =
## When the generic being instantiated had its body LOADED from a NIF (only
## `nim m`/`nim nifc`, only for a generic owned by another module), that body
## re-sems from plain identifiers — ast2nif serialises locals/params as idents,
## not `nkSym`. A `T: typedesc[...]` param referenced as a type must then
## resolve `T` to the bound type, but the instantiated skParam carries the
## concrete type `instantiateProcType` typedesc-skipped it to, which an ident
## lookup cannot use as a type name. Shadow each such param with an `skType`
## alias of the same name in a fresh scope layer (the alias is exactly how Nim
## models "this name denotes a type"). In-process bodies reach the param as
## `nkSym` and never take this path, hence the command gate.
##
## Returns true iff a scope layer was opened; the caller must `closeScope`.
if c.config.cmd notin {cmdM, cmdNifC} or orig == nil or
orig.itemId.module == c.module.position or
orig.typ == nil or orig.typ.n == nil:
return false
result = false
let procParams = instantiated.typ.n
for i in 1..<min(procParams.len, orig.typ.n.len):
if orig.typ.n[i].kind != nkSym: continue
let origParamTyp = orig.typ.n[i].sym.typ
if origParamTyp != nil and origParamTyp.kind == tyTypeDesc and
tfUnresolved in origParamTyp.flags:
if not result:
openScope(c)
result = true
let p = procParams[i].sym
let alias = newSym(skType, p.name, c.idgen, instantiated, p.info)
alias.typ = p.typ
addDecl(c, alias)
proc instantiateBody(c: PContext, n, params: PNode, result, orig: PSym) =
if n[bodyPos].kind != nkEmpty:
let procParams = result.typ.n
for i in 1..<procParams.len:
addDecl(c, procParams[i].sym)
let aliasLayer = aliasLoadedTypedescParams(c, result, orig)
maybeAddResult(c, result, result.ast)
inc c.inGenericInst
@@ -152,6 +185,7 @@ proc instantiateBody(c: PContext, n, params: PNode, result, orig: PSym) =
excl(result, sfForward)
trackProc(c, result, result.ast[bodyPos])
dec c.inGenericInst
if aliasLayer: closeScope(c)
proc fixupInstantiatedSymbols(c: PContext, s: PSym) =
for i in 0..<c.generics.len:
@@ -245,7 +279,7 @@ proc instantiateProcType(c: PContext, pt: LayeredIdTable,
let originalParams = result.n
result.n = originalParams.shallowCopy
for i in 1 ..< originalParams.len:
let resulti = originalParams[i].sym.typ
var resulti = originalParams[i].sym.typ
# twrong_field_caching requires these 'resetIdTable' calls:
if i > FirstParamAt:
resetIdTable(cl.symMap)
@@ -258,6 +292,11 @@ proc instantiateProcType(c: PContext, pt: LayeredIdTable,
let needsStaticSkipping = resulti.kind == tyFromExpr
let needsTypeDescSkipping = resulti.kind == tyTypeDesc and tfUnresolved in resulti.flags
if resulti.kind == tyFromExpr:
if resulti.state == Sealed:
# The generic was loaded from a NIF; do not brand the shared original.
# A tyFromExpr is a placeholder that `replaceTypeVarsT` resolves away,
# so a copy carries no identity that later comparisons could miss.
resulti = copyType(resulti, c.idgen, resulti.owner)
resulti.incl tfNonConstExpr
var paramType = replaceTypeVarsT(cl, resulti)
if needsStaticSkipping:
@@ -276,6 +315,12 @@ proc instantiateProcType(c: PContext, pt: LayeredIdTable,
let param = copySym(oldParam, c.idgen)
setOwner(param, prc)
param.typ = paramType
when defined(icDbgRefc):
echo "[icInst] ", prc.name.s, " param ", oldParam.name.s,
": ", typeToString(resulti), " (kind=", resulti.kind,
" uid=", resulti.uniqueId.module, ".", resulti.uniqueId.item,
" flags=", resulti.flags, ") -> ", typeToString(paramType),
" (kind=", paramType.kind, ")"
# The default value is instantiated and fitted against the final
# concrete param type. We avoid calling `replaceTypeVarsN` on the
@@ -283,6 +328,9 @@ proc instantiateProcType(c: PContext, pt: LayeredIdTable,
if oldParam.ast != nil:
var def = oldParam.ast.copyTree
if def.typ.kind == tyFromExpr:
if def.typ.state == Sealed:
# `copyTree` shares types; see the `resulti` comment above.
def.typ = copyType(def.typ, c.idgen, def.typ.owner)
def.typ.incl tfNonConstExpr
if not isIntLit(def.typ):
def = prepareNode(cl, def)
@@ -374,6 +422,11 @@ proc generateInstance(c: PContext, fn: PSym, pt: LayeredIdTable,
## parameters to their concrete types within the generic instance.
# no need to instantiate generic templates/macros:
internalAssert c.config, fn.kind notin {skMacro, skTemplate}
# IC: instantiating `fn` consumes its generic body in the current module's
# sem — record a NeedsImpl (strong) edge to `fn`'s module. The iface cookie
# hashes only signatures now, so a generic body edit moves only the impl
# cookie, and just the modules that instantiated it re-sem.
recordIcImplDep(c.graph, fn)
# generates an instantiated proc
if c.instCounter > 50:
globalError(c.config, info, "generic instantiation too nested")
@@ -455,6 +508,10 @@ proc generateInstance(c: PContext, fn: PSym, pt: LayeredIdTable,
# This is needed for cyclic module dependencies where generic instances
# may be created in one module but referenced from another.
logGenericInstance(c.graph, result)
# Under IC the instance's NIF name must be canonical across modules:
# derive its `disamb` from the instantiation identity (generic +
# concrete types) instead of the per-module counter.
setInstanceDisamb(c.graph, result, fn, entry.concreteTypes)
# bug #12985 bug #22913
# TODO: use the context of the declaration of generic functions instead
# TODO: consider fixing options as well

View File

@@ -43,17 +43,8 @@ proc semAddr(c: PContext; n: PNode): PNode =
result.typ = makePtrType(c, x.typ.skipTypes({tySink}))
proc semTypeOf(c: PContext; n: PNode): PNode =
var m = BiggestInt 1 # typeOfIter
if n.len == 3:
let mode = semConstExpr(c, n[2])
if mode.kind != nkIntLit:
localError(c.config, n.info, "typeof: cannot evaluate 'mode' parameter at compile-time")
else:
m = mode.intVal
let typExpr = semTypeOfImpl(c, n)
result = newNodeI(nkTypeOfExpr, n.info)
inc c.inTypeofContext
defer: dec c.inTypeofContext # compiles can raise an exception
let typExpr = semExprWithType(c, n[1], if m == 1: {efInTypeof} else: {})
result.add typExpr
if typExpr.typ.kind == tyFromExpr:
typExpr.typ.incl tfNonConstExpr

View File

@@ -497,6 +497,33 @@ proc addRaiseEffect(a: PEffects, e, comesFrom: PNode) =
if not isDefectException(e.typ):
throws(a.exc, e, comesFrom)
proc skipHiddenConv(n: PNode): PNode =
result = n
while true:
case result.kind
of nkHiddenStdConv, nkHiddenSubConv:
result = result[1]
else: break
proc addRaiseEffectsFromExpr(a: PEffects, e, comesFrom: PNode) =
if e.isNil:
return
case e.kind
of nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr:
if e.len > 0:
addRaiseEffectsFromExpr(a, e.lastSon.skipHiddenConv, comesFrom)
of nkIfExpr, nkIfStmt:
for branch in items(e):
if branch.len > 0:
addRaiseEffectsFromExpr(a, branch.lastSon.skipHiddenConv, comesFrom)
of nkCaseStmt:
for i in 1..<e.len:
let branch = e[i]
if branch.len > 0:
addRaiseEffectsFromExpr(a, branch.lastSon.skipHiddenConv, comesFrom)
else:
addRaiseEffect(a, e, comesFrom)
proc addTag(a: PEffects, e, comesFrom: PNode) =
var aa = a.tags
for i in 0..<aa.len:
@@ -1208,6 +1235,7 @@ type
enforcedGcSafety, enforceNoSideEffects: bool
oldExc, oldTags, oldForbids: int
exc, tags, forbids: PNode
excSource, tagsSource, forbidsSource: PNode
proc createBlockContext(tracked: PEffects): PragmaBlockContext =
var oldForbidsLen = 0
@@ -1230,17 +1258,18 @@ proc unapplyBlockContext(tracked: PEffects; bc: PragmaBlockContext) =
# anything about 'raises' in the 'cast' at all. Same applies for 'tags'.
setLen(tracked.exc.sons, bc.oldExc)
for e in bc.exc:
addRaiseEffect(tracked, e, e)
addRaiseEffect(tracked, e, if bc.excSource != nil: bc.excSource else: e)
if bc.tags != nil:
setLen(tracked.tags.sons, bc.oldTags)
for t in bc.tags:
addTag(tracked, t, t)
addTag(tracked, t, if bc.tagsSource != nil: bc.tagsSource else: t)
if bc.forbids != nil:
setLen(tracked.forbids.sons, bc.oldForbids)
for t in bc.forbids:
addNotTag(tracked, t, t)
addNotTag(tracked, t, if bc.forbidsSource != nil: bc.forbidsSource else: t)
proc castBlock(tracked: PEffects, pragma: PNode, bc: var PragmaBlockContext) =
proc castBlock(tracked: PEffects, castPragma: PNode, bc: var PragmaBlockContext) =
let pragma = castPragma[1]
case whichPragma(pragma)
of wGcSafe:
bc.enforcedGcSafety = true
@@ -1253,6 +1282,7 @@ proc castBlock(tracked: PEffects, pragma: PNode, bc: var PragmaBlockContext) =
else:
bc.tags = newNodeI(nkArgList, pragma.info)
bc.tags.add n
bc.tagsSource = castPragma
of wForbids:
let n = pragma[1]
if n.kind in {nkCurly, nkBracket}:
@@ -1260,6 +1290,7 @@ proc castBlock(tracked: PEffects, pragma: PNode, bc: var PragmaBlockContext) =
else:
bc.forbids = newNodeI(nkArgList, pragma.info)
bc.forbids.add n
bc.forbidsSource = castPragma
of wRaises:
let n = pragma[1]
if n.kind in {nkCurly, nkBracket}:
@@ -1267,6 +1298,7 @@ proc castBlock(tracked: PEffects, pragma: PNode, bc: var PragmaBlockContext) =
else:
bc.exc = newNodeI(nkArgList, pragma.info)
bc.exc.add n
bc.excSource = castPragma
of wUncheckedAssign:
discard "handled in sempass1"
else:
@@ -1303,6 +1335,8 @@ proc allowCStringConv(n: PNode): bool =
proc track(tracked: PEffects, n: PNode) =
case n.kind
of nkTypeOfExpr:
discard "typeof() never evaluates its operand; not a definite-assignment use"
of nkSym:
useVar(tracked, n)
if n.sym.typ != nil and tfHasAsgn in n.sym.typ.flags:
@@ -1319,7 +1353,7 @@ proc track(tracked: PEffects, n: PNode) =
if n[0].kind != nkEmpty:
n[0].info = n.info
#throws(tracked.exc, n[0])
addRaiseEffect(tracked, n[0], n)
addRaiseEffectsFromExpr(tracked, n[0], n)
for i in 0..<n.safeLen:
track(tracked, n[i])
createTypeBoundOps(tracked, n[0].typ, n.info)
@@ -1520,7 +1554,7 @@ proc track(tracked: PEffects, n: PNode) =
of wNoSideEffect:
bc.enforceNoSideEffects = true
of wCast:
castBlock(tracked, pragmaList[i][1], bc)
castBlock(tracked, pragmaList[i], bc)
else:
discard
applyBlockContext(tracked, bc)

View File

@@ -2625,6 +2625,14 @@ proc semProcAux(c: PContext, n: PNode, kind: TSymKind,
n[genericParamsPos] = proto.ast[genericParamsPos]
n[paramsPos] = proto.ast[paramsPos]
n[pragmasPos] = proto.ast[pragmasPos]
# miscPos holds this definition's *original* generic-param node (kept for
# error messages, see setGenericParamsMisc / issue #1713). For an impl that
# resolves to a forward decl, that node was analysed under the now-discarded
# impl symbol and its generic-param constraint types are owned by it. Adopt
# the prototype's miscPos so the discarded impl sym is fully unreachable —
# otherwise it leaks (via `proto.ast = n` below) as a type owner and gets
# serialized as a phantom duplicate overload under IC.
n[miscPos] = proto.ast[miscPos]
if n[namePos].kind != nkSym: internalError(c.config, n.info, "semProcAux")
n[namePos].sym = proto
if importantComments(c.config) and proto.ast.comment.len > 0:
@@ -2642,6 +2650,11 @@ proc semProcAux(c: PContext, n: PNode, kind: TSymKind,
elif s.name.s == "()" and callOperator notin c.features:
localError(c.config, n.info, "the overloaded " & s.name.s &
" operator has to be enabled with {.experimental: \"callOperator\".}")
elif sfImportc notin s.flags and (s.name.s == ">" or s.name.s == ">=" or s.name.s == "!="):
# ignore imported procs as these operators in backend language might have different semantics
let op1 = if s.name.s == "!=": "==" elif s.name.s == ">": "<" else: "<="
message(c.config, n.info, warnInvalidCmpOp, "define `" & op1 & "` instead of `" & s.name.s & "` to implement user defined comparison operator. " &
"it allows you to use `" & s.name.s & "` automatically.")
if sfBorrow in s.flags and c.config.cmd notin cmdDocLike:
result[bodyPos] = c.graph.emptyNode

View File

@@ -512,7 +512,13 @@ proc semArrayIndex(c: PContext, n: PNode): PType =
if c.inGenericContext > 0: result.incl tfUnresolved
else:
result = e.typ.skipTypes({tyTypeDesc})
result.incl tfImplicitStatic
if result.state != Sealed:
# For a type loaded from the IC cache we skip the flag instead of
# mutating (or copying) the type: tfImplicitStatic has no readers in
# the compiler, and a copy would get a fresh itemId, breaking enum
# identity (`sameEnumTypes` compares ids) — `arr[enumVal]` on an
# `array[LoadedEnum, T]` would no longer typecheck.
result.incl tfImplicitStatic
elif e.kind in (nkCallKinds + {nkBracketExpr}) and hasUnresolvedArgs(c, e):
if not isOrdinalType(e.typ.skipTypes({tyStatic, tyAlias, tyGenericInst, tySink})):
localError(c.config, n[1].info, errOrdinalTypeExpected % typeToString(e.typ, preferDesc))
@@ -1355,7 +1361,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
var staticCopy = paramType[i].exactReplica(c.idgen)
staticCopy.incl tfInferrableStatic
result.rawAddSon staticCopy
else:
@@ -1891,6 +1897,12 @@ proc semTypeExpr(c: PContext, n: PNode; prev: PType): PType =
# by macros. Only macros can summon unnamed types
# and cast spell upon AST. Here we need to give
# it a name taken from left hand side's node
if result.state == Sealed:
# The unnamed type was loaded from a dependency's NIF and must not
# be mutated in place; attach the name to a fresh copy instead.
let orig = result
result = copyType(orig, c.idgen, getCurrOwner(c))
copyTypeProps(c.graph, c.idgen.module, result, orig)
result.sym = prev.sym
result.sym.typ = result
else:
@@ -2063,6 +2075,57 @@ proc semStaticType(c: PContext, childNode: PNode, prev: PType): PType =
result.rawAddSon(base)
result.incl tfHasStatic
proc semTypeOfImpl(c: PContext; n: PNode): PNode =
var m = BiggestInt 1 # typeOfIter
var modifierMode = BiggestInt 0 # CompatibleTypeModifiers
type
TypeOfParams = enum
topMode
topModifier
if n.len in 3 .. 4:
for i in 2 ..< n.len:
var argKind = topMode
var arg: PNode = nil
if n[i].kind == nkExprEqExpr and n[i][0].kind == nkIdent:
# named param
case n[i][0].ident.s
of "mode": argKind = topMode
of "modifierMode": argKind = topModifier
else:
localError(c.config, n.info, "typeof: got unknown parameter name")
arg = n[i][1]
else:
if i == 2:
argKind = topMode
else:
argKind = topModifier
arg = n[i]
case argKind
of topMode:
let mode = semConstExpr(c, arg)
if mode.kind != nkIntLit:
localError(c.config, n.info, "typeof: cannot evaluate 'mode' parameter at compile-time")
else:
m = mode.intVal
of topModifier:
let modMode = semConstExpr(c, arg)
if modMode.kind != nkIntLit:
localError(c.config, n.info, "typeof: cannot evaluate 'modifierMode' parameter at compile-time")
else:
modifierMode = modMode.intVal
inc c.inTypeofContext
defer: dec c.inTypeofContext # compiles can raise an exception
var typExpr = semExprNoDeref(c, n[1], if m == 1: {efInTypeof} else: {})
if modifierMode == 0:
# CompatibleTypeModifiers
typExpr.typ = typExpr.typ.skipTypes({tyVar, tyLent})
elif modifierMode == 1:
# RemoveTypeModifiers
typExpr.typ = typExpr.typ.skipTypes({tyVar, tyLent, tySink})
result = typExpr
proc semTypeOf(c: PContext; n: PNode; prev: PType): PType =
openScope(c)
inc c.inTypeofContext
@@ -2083,16 +2146,7 @@ proc semTypeOf(c: PContext; n: PNode; prev: PType): PType =
proc semTypeOf2(c: PContext; n: PNode; prev: PType): PType =
openScope(c)
var m = BiggestInt 1 # typeOfIter
if n.len == 3:
let mode = semConstExpr(c, n[2])
if mode.kind != nkIntLit:
localError(c.config, n.info, "typeof: cannot evaluate 'mode' parameter at compile-time")
else:
m = mode.intVal
inc c.inTypeofContext
defer: dec c.inTypeofContext # compiles can raise an exception
let ex = semExprWithType(c, n[1], if m == 1: {efInTypeof} else: {})
let ex = semTypeOfImpl(c, n)
closeScope(c)
result = ex.typ
if result.kind == tyFromExpr:
@@ -2136,7 +2190,7 @@ proc semTypeIdent(c: PContext, n: PNode): PSym =
localError(c.config, n.info, errTypeExpected)
return errorSym(c, n)
result = result.typ.sym.copySym(c.idgen)
result.typ = exactReplica(result.typ)
result.typ = exactReplica(result.typ, c.idgen)
result.typ.incl tfUnresolved
if result.kind == skGenericParam:

View File

@@ -272,10 +272,17 @@ proc replaceTypeVarsN(cl: var TReplTypeVars, n: PNode; start=0; expectedType: PT
if n == nil: return
result = copyNode(n)
if n.typ != nil:
if n.typ.kind == tyFromExpr:
var nodeTyp = n.typ
if nodeTyp.kind == tyFromExpr:
# type of node should not be evaluated as a static value
n.typ.incl tfNonConstExpr
result.typ = replaceTypeVarsT(cl, n.typ)
if nodeTyp.state == Sealed:
# IC: do not brand the loaded shared original — a tyFromExpr is a
# placeholder that `replaceTypeVarsT` resolves away, so the copy
# carries no identity later comparisons could miss (mirrors
# `instantiateProcType`)
nodeTyp = copyType(nodeTyp, cl.c.idgen, nodeTyp.owner)
nodeTyp.incl tfNonConstExpr
result.typ = replaceTypeVarsT(cl, nodeTyp)
checkMetaInvariants(cl, result.typ)
case n.kind
of nkNone..pred(nkSym), succ(nkSym)..nkNilLit:
@@ -387,6 +394,13 @@ proc lookupTypeVar(cl: var TReplTypeVars, t: PType): PType =
# don't bind `auto` return type to a previous binding of `auto`
return nil
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,
" state=", t.state, " flags=", t.flags, " -> ",
(if result != nil: typeToString(result) else: "MISS"),
" allowMeta=", cl.allowMetaTypes
if result == nil:
if cl.allowMetaTypes or tfRetType in t.flags: return
localError(cl.c.config, t.sym.info, "cannot instantiate: '" & typeToString(t) & "'")
@@ -401,7 +415,7 @@ proc lookupTypeVar(cl: var TReplTypeVars, t: PType): PType =
proc instCopyType*(cl: var TReplTypeVars, t: PType): PType =
# XXX: relying on allowMetaTypes is a kludge
if cl.allowMetaTypes:
result = t.exactReplica
result = t.exactReplica(cl.c.idgen)
else:
result = copyType(t, cl.c.idgen, t.owner)
copyTypeProps(cl.c.graph, cl.c.idgen.module, result, t)
@@ -446,6 +460,13 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
header[i] = x
propagateToOwner(header, x)
else:
# Under IC `t` may be a loaded dep type (Sealed/immutable); mutating it
# would assert, so propagate into a copy. For non-Sealed types keep
# devel's in-place propagation: unconditionally copying here changes
# `header != t` and with it the cached-instance lookup below, which
# regressed non-IC generic instantiations (arraymancer: a cached
# NimSeqV2 instance with stale flags was returned for a cast target).
if header == t and t.state == Sealed: header = instCopyType(cl, t)
propagateToOwner(header, x)
if header != t:
@@ -497,8 +518,14 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
let bbody = last body
var newbody = replaceTypeVarsT(cl, bbody, isInstValue = true)
cl.skipTypedesc = oldSkipTypedesc
newbody.flags = newbody.flags + (t.flags + body.flags - tfInstClearedFlags)
result.flags = result.flags + newbody.flags - tfInstClearedFlags
let newbodyFlags = newbody.flags + (t.flags + body.flags - tfInstClearedFlags)
if newbody.state != Sealed:
newbody.flags = newbodyFlags
# else: `newbody` is a type loaded from a dep module (it can even be a
# builtin like `int` when the generic's body is computed by a macro) and is
# immutable under IC. Skip the in-place flag accumulation on the shared
# type; the instance `result` still receives the flags below.
result.flags = result.flags + newbodyFlags - tfInstClearedFlags
setToPreviousLayer(cl.typeMap)
@@ -518,8 +545,11 @@ proc handleGenericInvocation(cl: var TReplTypeVars, t: PType): PType =
# generics *when the type is constructed*:
cl.c.graph.setAttachedOp(cl.c.module.position, newbody, attachedDeepCopy,
cl.c.instTypeBoundOp(cl.c, dc, result, cl.info, attachedDeepCopy, 1))
if newbody.typeInst == nil:
if newbody.typeInst == nil and newbody.state != Sealed:
# doAssert newbody.typeInst == nil
# An IC-loaded (Sealed) `newbody` keeps whatever `typeInst` its defining
# module serialized; recording this process's first instantiation on the
# shared type is not possible (and was always first-wins anyway).
newbody.typeInst = result
if tfRefsAnonObj in newbody.flags and newbody.kind != tyGenericInst:
# can come here for tyGenericInst too, see tests/metatype/ttypeor.nim
@@ -808,7 +838,11 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
if t.kind == tyRef and t.hasElementType and t.elementType.kind == tyObject and t.elementType.n != nil:
discard replaceObjBranches(cl, t.elementType.n)
elif result.n != nil and t.kind == tyObject:
elif result.n != nil and t.kind == tyObject and result.state != Sealed:
# A type loaded from the IC cache already had its object branches
# resolved when it was originally compiled, and must not be mutated in
# place (nor copied, which would break object-inheritance identity), so
# only non-Sealed types are processed here.
# Invalidate the type size as we may alter its structure
result.size = -1
result.n = replaceObjBranches(cl, result.n)
@@ -860,7 +894,10 @@ proc recomputeFieldPositions*(t: PType; obj: PNode; currPosition: var int) =
for i in 1..<obj.len:
recomputeFieldPositions(nil, lastSon(obj[i]), currPosition)
of nkSym:
obj.sym.position = currPosition
# A field loaded from the IC cache is already at its final position and must
# not be mutated; only freshly instantiated fields need (re)positioning.
if obj.sym.state != Sealed:
obj.sym.position = currPosition
inc currPosition
else: discard "cannot happen"

View File

@@ -52,7 +52,17 @@ proc hashSym(c: var MD5Context, s: PSym) =
c &= ":anon"
else:
var it = s
when defined(icDbgHash):
var ownerSteps = 0
while it != nil:
when defined(icDbgHash):
inc ownerSteps
if ownerSteps >= 1000 and ownerSteps <= 1030:
echo "OWNERLOOP(hashSym) n=", ownerSteps, " sym=", it.name.s, " kind=", it.kind,
" id=", it.itemId, " flags=", it.flags, " state=", it.state,
" start=", s.name.s, " startId=", s.itemId
elif ownerSteps == 1031:
raiseAssert "owner-chain cycle detected, see OWNERLOOP dump above"
c &= it.name.s
c &= "."
it = it.owner
@@ -65,7 +75,17 @@ proc hashTypeSym(c: var MD5Context, s: PSym; conf: ConfigRef) =
else:
var it = s
c &= customPath(conf.toFullPath(s.info))
when defined(icDbgHash):
var ownerSteps = 0
while it != nil:
when defined(icDbgHash):
inc ownerSteps
if ownerSteps >= 1000 and ownerSteps <= 1030:
echo "OWNERLOOP n=", ownerSteps, " sym=", it.name.s, " kind=", it.kind,
" id=", it.itemId, " flags=", it.flags, " state=", it.state,
" start=", s.name.s, " startId=", s.itemId
elif ownerSteps == 1031:
raiseAssert "owner-chain cycle detected, see OWNERLOOP dump above"
if sfFromGeneric in it.flags and it.kind in routineKinds and
it.typ != nil:
hashType c, it.typ, {CoProc}, conf
@@ -102,15 +122,44 @@ proc hashTree(c: var MD5Context, n: PNode; flags: set[ConsiderFlag]; conf: Confi
else:
for i in 0..<n.len: hashTree(c, n[i], flags, conf)
when defined(icDbgHash):
var hashDepth = 0
var hashCalls = 0
var hashMaxDepth = 0
proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]; conf: ConfigRef) =
if t == nil:
c &= "\254"
return
when defined(icDbgHash):
inc hashDepth
inc hashCalls
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"),
" state=", t.state, " owner=", (if t.owner != nil: t.owner.name.s else: "NIL")
elif hashCalls == 500_000_301:
echo "HASHLOOP maxDepth=", hashMaxDepth
raiseAssert "hashType runaway detected, see HASHLOOP dump above"
defer:
dec hashDepth
# Ensure type is fully loaded before hashing to avoid hash changing
# as properties are accessed and trigger lazy loading.
backendEnsureMutable(t)
# Bare type-class keywords used as a typedesc without arguments (e.g. `array`,
# `range`, `distinct` passed to `signatureHash`) have no children, so the
# structural branches below would index a non-existent `elementType`. Hash them
# by kind (+ sym for an extra, stable distinction) — enough for a stable,
# distinct identity. (`seq`/`openArray`/`tuple` already fall through the empty
# `else` loop unharmed; this covers the branches that index `elementType`.)
if t.kind in {tyArray, tyRange, tyDistinct} and not t.hasElementType:
c &= char(t.kind)
if t.sym != nil: c.hashSym(t.sym)
return
case t.kind
of tyGenericInvocation:
for a in t.kids:
@@ -248,6 +297,29 @@ proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]; conf: Confi
c.hashType(param.typ, flags, conf)
c &= ','
c.hashType(t.returnType, flags, conf)
elif t.n != nil and t.n.kind == nkFormalParams:
# Under IC a loaded proc type stores its parameters only in `n`; `sons`
# holds just the return type. Hashing `t.signature` would silently drop
# every parameter, collapsing distinct proc types onto one hash, so the
# same logical type got different C struct names in different TUs
# ("incompatible type for argument" on closure args). Hash the return
# type first and then the parameter types from `n` — for from-source
# types `n`'s param types equal `sons[1..]`, so non-IC hashes are
# unchanged. (Same fix as typekeys' tyProc branch.)
c.hashType(t.returnType, flags, conf)
for i in 1..<t.n.len:
let p = t.n[i]
if p.kind == nkSym:
backendEnsureMutable(p.sym)
# The hidden closure env param: under IC, lambda lifting shares the
# routine's AST params with `typ.n`, so the lifted `:envP` leaks into
# the TYPE's params (from-source types never carry it). It is not part
# of the type's identity — `genProcParams` skips it the same way.
if t.callConv == ccClosure and p.sym.name.s == ":envP":
continue
c.hashType(p.sym.typ, flags, conf)
else:
c.hashType(p.typ, flags, conf)
else:
for a in t.signature: c.hashType(a, flags, conf)
c &= char(t.callConv)
@@ -263,6 +335,21 @@ proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]; conf: Confi
c &= char(t.kind)
c.hashType(t.indexType, flags-{CoIgnoreRange}+{CoIgnoreRangeInArray}, conf)
c.hashType(t.elementType, flags-{CoIgnoreRange}, conf)
of tyBuiltInTypeClass:
# A builtin type class (`object`, `tuple`, `proc`, `ref`, `seq`, ...) is
# identified solely by the *kind* of its single placeholder son plus a few
# flags/callConv (see `sameType`). That son is a fresh, field-less, sym-less
# type, so the generic `else` below would recurse into it and hash its
# process-local `t.id` — unstable across the NIF boundary. nim-serialization
# keys auto-serialization on `signatureHash(object)`/`tuple`/... and missed
# under IC because the registering and consuming modules minted different
# placeholder ids. Hash the class identity that `sameType` actually compares.
c &= char(t.kind)
let elem = t.elementType
c &= char(elem.kind)
for f in eqTypeFlags * elem.flags: c &= char(ord(f))
if elem.kind == tyProc and tfExplicitCallConv in elem.flags:
c &= char(elem.callConv)
else:
c &= char(t.kind)
for a in t.kids: c.hashType(a, flags, conf)

View File

@@ -135,6 +135,11 @@ proc put(c: var TCandidate, key, val: PType) {.inline.} =
writeStackTrace()
if c.c.module.name.s == "temp3":
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,
" state=", key.state, " -> ", typeToString(val)
put(c.bindings, key, val.skipIntLit(c.c.idgen))
proc typeRel*(c: var TCandidate, f, aOrig: PType,
@@ -791,8 +796,10 @@ proc procParamTypeRel(c: var TCandidate; f, a: PType): TTypeRelation =
# different C types (size_t vs unsigned long long).
let fCheck = concreteType(c, f)
let aCheck = concreteType(c, a)
# Note that `result` is equal; now check whether they have the same
# backend type.
if fCheck != nil and aCheck != nil and
not sameBackendTypePickyAliases(fCheck, aCheck):
not sameBackendTypePickyAliases(fCheck, aCheck, {IgnoreFlags}):
result = isNone
if result <= isSubrange or inconsistentVarTypes(f, a):
@@ -909,7 +916,7 @@ proc matchUserTypeClass*(m: var TCandidate; ff, a: PType): PType =
case typ.kind
of tyStatic:
param = paramSym skConst
param.typ = typ.exactReplica
param.typ = typ.exactReplica(m.c.idgen)
#copyType(typ, c.idgen, typ.owner)
if typ.n == nil:
param.typ.incl tfInferrableStatic
@@ -917,7 +924,7 @@ proc matchUserTypeClass*(m: var TCandidate; ff, a: PType): PType =
param.ast = typ.n
of tyFromExpr:
param = paramSym skVar
param.typ = typ.exactReplica
param.typ = typ.exactReplica(m.c.idgen)
#copyType(typ, c.idgen, typ.owner)
else:
param = paramSym skType
@@ -970,7 +977,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
result = ff.exactReplica(m.c.idgen)
#copyType(ff, c.idgen, ff.owner)
result.n = checkedBody
@@ -1166,6 +1173,10 @@ proc enterConceptMatch(c: var TCandidate; f,a: PType, flags: TTypeRelFlags): TTy
if concpt.kind != tyConcept:
container = concpt
concpt = container.reduceToBase
# considerPreviousT-like behavior
let prev = lookup(c.bindings, concpt)
if prev != nil:
return typeRel(c, prev, a, flags)
if trDontBind in flags:
conceptFlags.incl mfDontBind
if trCheckGeneric in flags:
@@ -1757,6 +1768,21 @@ proc typeRel(c: var TCandidate, f, aOrig: PType,
let ff = last(f)
if ff != nil:
result = typeRel(c, ff, a, flags)
if result == isNone and a.kind == tyGenericInst and trBindGenericParam in flags:
var depth = -1
# Generic-parameter constraints like `F: Future` can miss in `last(f)`
# when the actual type inherits from a concrete generic instantiation.
# Keep this fallback scoped to generic-parameter matching so typedesc
# overloads such as `type Future[T]` still prefer more specific
# descendants like `InternalRaisesFuture[T, E]`.
if isGenericSubtype(c, a, f, depth, f) and depth > 0:
var askip = skippedNone
let aobj = a.skipToObject(askip)
if aobj != nil and tfFinal notin aobj.flags:
# Keep overload ranking consistent with other inheritance-based
# matches: deeper descendants are slightly worse candidates.
inc c.inheritancePenalty, depth + int(c.inheritancePenalty < 0)
result = isGeneric
of tyGenericInvocation:
var x = a.skipGenericAlias
if x.kind == tyGenericParam and x.len > 0:
@@ -2471,6 +2497,10 @@ proc paramTypesMatchAux(m: var TCandidate, f, a: PType,
return arg
elif f.kind == tyStatic and arg.typ.n != nil:
return arg.typ.n
elif f.kind == tyUntyped:
# bug #25693: a different overload candidate may have sem-checked the
# operand and left symbols behind; templates expect the pristine AST.
return argOrig
else:
return argSemantized # argOrig
@@ -2645,7 +2675,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)
copiedNode.typ = exactReplica(copiedNode.typ, m.c.idgen)
copiedNode.typ.n = arg
arg = copiedNode
typeRel(m, f, arg.typ)
@@ -2849,6 +2879,7 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int
if m.calleeSym != nil and m.calleeSym.kind notin {skTemplate, skMacro}:
c.mergeShadowScope
else:
c.rememberShadowDefs
c.closeShadowScope
m.state = csNoMatch
m.firstMismatch.arg = a
@@ -2905,7 +2936,10 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int
setSon(m.call, formal.position + 1, container)
else:
incrIndexType(container.typ)
container.add n[a]
# bug #25693: like the scalar `tyUntyped` case in `paramTypesMatchAux`,
# a previous overload candidate may have sem-checked the operand in
# place; templates/macros expect the pristine AST, so use `nOrig`.
container.add nOrig[a]
elif n[a].kind == nkExprEqExpr:
# named param
m.firstMismatch.kind = kUnknownNamedParam
@@ -3004,7 +3038,8 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int
setSon(m.call, formal.position + 1, container)
else:
incrIndexType(container.typ)
container.add n[a]
# bug #25693: see the leading isVarargsUntyped branch above.
container.add nOrig[a]
else:
m.baseTypeMatch = false
m.typedescMatched = false
@@ -3056,6 +3091,7 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int
if m.state == csMatch and not (m.calleeSym != nil and m.calleeSym.kind in {skTemplate, skMacro}):
c.mergeShadowScope
else:
c.rememberShadowDefs
c.closeShadowScope
inc a

View File

@@ -394,9 +394,10 @@ proc computeSizeAlign(conf: ConfigRef; typ: PType) =
accum.offset = 1
computeObjectOffsetsFoldFunction(conf, typ.n, false, accum)
let paddingAtEnd = int16(accum.finish())
if typ.sym != nil and
typ.sym.flags * {sfCompilerProc, sfImportc} == {sfImportc} and
tfCompleteStruct notin typ.flags:
if (typ.sym != nil and
typ.sym.flags * {sfCompilerProc, sfImportc} == {sfImportc} and
tfCompleteStruct notin typ.flags) or
tfIncompleteStruct in typ.flags:
typ.size = szUnknownSize
typ.align = szUnknownSize
typ.paddingAtEnd = szUnknownSize

View File

@@ -22,7 +22,7 @@ import std / tables
import
options, ast, astalgo, trees, msgs,
idents, renderer, types, semfold, magicsys, cgmeth,
idents, renderer, types, semfold, magicsys, cgmeth, parampatterns,
lowerings, liftlocals,
modulegraphs, lineinfos
@@ -90,11 +90,21 @@ proc getCurrOwner(c: PTransf): PSym =
if c.transCon != nil: result = c.transCon.owner
else: result = c.module
proc freshOwnedSym(c: PTransf; s, owner: PSym): PNode =
# We need to copy the symbol here because we might need to change its owner and
# we don't want to mess with the original symbol which might be used in other places.
# This can happen for example for iterators which are transformed multiple times when
# they are used in different contexts.
var fresh = copySym(s, c.idgen)
if fresh.kind notin routineKinds:
incl(fresh.flagsImpl, sfFromGeneric)
setOwner(fresh, owner)
result = newSymNode(fresh)
proc newTemp(c: PTransf, typ: PType, info: TLineInfo): PNode =
let r = newSym(skTemp, getIdent(c.graph.cache, genPrefix), c.idgen, getCurrOwner(c), info)
r.typ = typ #skipTypes(typ, {tyGenericInst, tyAlias, tySink})
incl(r.flagsImpl, sfFromGeneric)
let owner = getCurrOwner(c)
result = newSymNode(r)
proc transform(c: PTransf, n: PNode, noConstFold = false): PNode
@@ -185,11 +195,39 @@ proc transformSym(c: PTransf, n: PNode): PNode =
result = transformSymAux(c, n)
proc freshVar(c: PTransf; v: PSym): PNode =
let owner = getCurrOwner(c)
var newVar = copySym(v, c.idgen)
incl(newVar.flagsImpl, sfFromGeneric)
setOwner(newVar, owner)
result = newSymNode(newVar)
result = freshOwnedSym(c, v, getCurrOwner(c))
proc introduceNewRoutineHeaderSyms(c: PTransf; n: PNode; oldOwner, newOwner: PSym) =
# We need to introduce new symbols for the parameters and result of a routine when
# we copy it for inlining or closure generation.
# Otherwise, we would have multiple nodes referring to the same parameter symbols which
# can lead to problems when we need to change the owner of these symbols.
case n.kind
of nkSym:
if n.sym.owner == oldOwner:
c.transCon.mapping[n.sym.itemId] = freshOwnedSym(c, n.sym, newOwner)
of nkEmpty..pred(nkSym), succ(nkSym)..nkNilLit:
discard
else:
for i in 0..<n.len:
introduceNewRoutineHeaderSyms(c, n[i], oldOwner, newOwner)
proc copyRoutineTypeHeader(c: PTransf; oldProc, newProc: PSym) =
# We need to copy the routine type header to ensure that
# modifications to the newProc do not affect the oldProc.
if oldProc.typ != nil and oldProc.typ.kind == tyProc and oldProc.typ.n != nil:
newProc.typ = copyType(oldProc.typ, c.idgen, newProc)
newProc.typ.n = newNodeI(oldProc.typ.n.kind, oldProc.typ.n.info)
if oldProc.typ.n.len > 0:
newProc.typ.n.add copyTree(oldProc.typ.n[0])
for i in 1..<oldProc.typ.n.len:
let oldParam = oldProc.typ.n[i].sym
var newParam = getOrDefault(c.transCon.mapping, oldParam.itemId)
if newParam == nil:
newParam = freshOwnedSym(c, oldParam, newProc)
c.transCon.mapping[oldParam.itemId] = newParam
doAssert newParam.kind == nkSym
newProc.typ.addParam newParam.sym
proc transformVarSection(c: PTransf, v: PNode): PNode =
result = newTransNode(v)
@@ -338,11 +376,18 @@ proc introduceNewLocalVars(c: PTransf, n: PNode): PNode =
return n
of nkLambdaKinds, nkProcDef, nkFuncDef, nkMethodDef, nkConverterDef: # todo optimize nosideeffects?
result = newTransNode(n)
let x = newSymNode(copySym(n[namePos].sym, c.idgen))
c.transCon.mapping[n[namePos].sym.itemId] = x
let oldProc = n[namePos].sym
let x = freshOwnedSym(c, oldProc, oldProc.owner)
c.transCon.mapping[oldProc.itemId] = x
introduceNewRoutineHeaderSyms(c, n[paramsPos], oldProc, x.sym)
if resultPos < n.len and n[resultPos] != nil:
introduceNewRoutineHeaderSyms(c, n[resultPos], oldProc, x.sym)
copyRoutineTypeHeader(c, oldProc, x.sym)
result[namePos] = x # we have to copy proc definitions for iters
for i in 1..<n.len:
result[i] = introduceNewLocalVars(c, n[i])
if x.sym.typ != nil and x.sym.typ.kind == tyProc:
result[paramsPos] = x.sym.typ.n
result[namePos].sym.ast = result
else:
result = newTransNode(n)
@@ -675,7 +720,7 @@ type
paDirectMapping, paFastAsgn, paFastAsgnTakeTypeFromArg
paVarAsgn, paComplexOpenarray, paViaIndirection
proc putArgInto(arg: PNode, formal: PType): TPutArgInto =
proc putArgInto(arg: PNode, formal: PType; borrowedFirstArg = false): TPutArgInto =
# This analyses how to treat the mapping "formal <-> arg" in an
# inline context.
if formal.kind == tyTypeDesc: return paDirectMapping
@@ -726,6 +771,13 @@ proc putArgInto(arg: PNode, formal: PType): TPutArgInto =
if skipTypes(formal, abstractInst).kind in {tyVar, tyLent}: result = paVarAsgn
else: result = paFastAsgn
if borrowedFirstArg and result == paDirectMapping and parampatterns.exprRoot(arg) == nil and
parampatterns.isAssignable(nil, arg) == arNone:
# Inline iterators like `items(array)` borrow from the first argument.
# If that argument is just a transient expression, materialize it so the
# lifted closure keeps the backing storage alive across yields.
result = paFastAsgnTakeTypeFromArg
proc findWrongOwners(c: PTransf, n: PNode) =
if n.kind == nkVarSection:
let x = n[0][0]
@@ -824,13 +876,16 @@ proc transformFor(c: PTransf, n: PNode): PNode =
if iter.kind != skIterator: return result
# generate access statements for the parameters (unless they are constant)
pushTransCon(c, newC)
let borrowedIterResult =
iter.typ != nil and iter.typ.returnType != nil and
skipTypes(iter.typ.returnType, abstractInst).kind in {tyLent, tyVar}
for i in 1..<call.len:
var arg = transform(c, call[i])
let ff = skipTypes(iter.typ, abstractInst)
# can happen for 'nim check':
if i >= ff.n.len: return result
var formal = ff.n[i].sym
let pa = putArgInto(arg, formal.typ)
let pa = putArgInto(arg, formal.typ, borrowedIterResult and i == 1)
case pa
of paDirectMapping:
newC.mapping[formal.itemId] = arg

View File

@@ -10,7 +10,7 @@
## Based on sighashes.nim but works on astdef directly as we need it in ast2nif.nim.
## Also produces more readable names thanks to treemangler.
import std/assertions
import std/[assertions, sets]
import "../dist/nimony/src/lib" / [treemangler]
import "../dist/nimony/src/gear2" / modnames
@@ -54,6 +54,9 @@ type
m: Mangler
tl: TypeLoader
sl: SymLoader
visited: HashSet[ItemId] # anonymous object types whose fields are currently
# being hashed — a non-mutating guard against endless
# recursion when a field references the type itself.
proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef)
proc symKey(c: var Context; s: PSym; conf: ConfigRef) =
@@ -67,14 +70,26 @@ proc symKey(c: var Context; s: PSym; conf: ConfigRef) =
name.add '.'
name.addInt s.disamb
# The owner may still be an unloaded stub (kind `skStub`): force it in
# before inspecting its kind, otherwise the module suffix is silently
# dropped from the key and def-vs-use keys diverge — e.g. `Lexer`'s base
# class keyed as `TBaseLexer.0.` at nifc vs `TBaseLexer.0.nimqydn3y` at
# sem time, making `getAttachedOp` miss ("'=destroy' operator not found").
template forceLoaded(x: PSym): PSym =
let tmp = x
if tmp != nil and tmp.state == Partial and c.sl != nil: c.sl(tmp)
tmp
let owner = forceLoaded(s.ownerFieldImpl)
let it =
if s.kindImpl == skModule:
s
elif s.kindImpl in skProcKinds and sfFromGeneric in s.flagsImpl and s.ownerFieldImpl.kindImpl != skModule:
s.ownerFieldImpl.ownerFieldImpl
elif s.kindImpl in skProcKinds and sfFromGeneric in s.flagsImpl and
owner != nil and owner.kindImpl != skModule:
forceLoaded(owner.ownerFieldImpl)
else:
s.ownerFieldImpl
if it.kindImpl == skModule:
owner
if it != nil and it.kindImpl == skModule:
name.add '.'
name.add modname(it, conf)
c.m.addSymbol(name)
@@ -138,7 +153,12 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef
for a in t.sonsImpl:
c.typeKey a, flags, conf
of tyDistinct:
if CoDistinct in flags:
if t.sonsImpl.len == 0:
# a bare `distinct` typeclass (e.g. `foo(distinct, ...)` matched
# against a `T: type` param) has no base type to key — it IS its kind
withTree c.m, toNifTag(t.kind):
c.m.addEmpty()
elif CoDistinct in flags:
if t.symImpl != nil: symKey(c, t.symImpl, conf)
if t.symImpl == nil or tfFromGeneric in t.flagsImpl:
c.typeKey t.sonsImpl[^1], flags, conf
@@ -147,7 +167,14 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef
else:
symKey(c, t.symImpl, conf)
of tyGenericInst:
if sfInfixCall in t.sonsImpl[0].symImpl.flagsImpl:
# The generic head (son[0]) may be a lazily-loaded stub under IC; ensure it
# is materialised before peeking at its symbol. A nil sym means this is not
# an imported C++ generic, so fall through to the normal `skipModifierB`.
var base = t.sonsImpl[0]
if base.state == Partial:
assert c.tl != nil
c.tl(base)
if base.symImpl != nil and sfInfixCall in base.symImpl.flagsImpl:
# This is an imported C++ generic type.
# We cannot trust the `lastSon` to hold a properly populated and unique
# value for each instantiation, so we hash the generic parameters here:
@@ -216,14 +243,48 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef
if t.typeInstImpl != nil:
# prevent against infinite recursions here, see bug #8883:
let inst = t.typeInstImpl
if inst.state == Partial:
# a lazily-loaded typeInst stub has no sons until forced in
assert c.tl != nil
c.tl(inst)
t.typeInstImpl = nil # IC: spurious writes are ok since we set it back immediately
assert inst.kind == tyGenericInst
c.typeKey inst.sonsImpl[0], flags, conf
if inst.sonsImpl.len > 0:
c.typeKey inst.sonsImpl[0], flags, conf
for i in 1..<inst.sonsImpl.len-1:
c.typeKey inst.sonsImpl[i], flags, conf
# Match sighashes: generic-instantiation arguments are keyed with
# `CoDistinct` so distinct args are not collapsed to their base.
c.typeKey inst.sonsImpl[i], flags+{CoDistinct}, conf
t.typeInstImpl = inst
elif t.symImpl != nil:
c.symKey(t.symImpl, conf)
# Anonymous / gensym'd object types (e.g. closure environments and
# `ref object` ObjectTypes) share the placeholder name `´anon`, so `symKey`
# alone collapses every one of them onto the same key — which made distinct
# closure-env `=destroy`/`=sink` hooks collide. Mirror sighashes: when the
# type symbol is anonymous/gensym'd, disambiguate further by keying the
# field types and names (or `.empty` when there are none).
template hasFlag(sym: PSym): bool =
{sfAnon, sfGenSym} * sym.flagsImpl != {}
if hasFlag(t.symImpl) or
(t.kind == tyObject and t.ownerFieldImpl != nil and t.ownerFieldImpl.kindImpl == skType and
t.ownerFieldImpl.typImpl != nil and t.ownerFieldImpl.typImpl.kind == tyRef and hasFlag(t.ownerFieldImpl)):
if t.nImpl != nil and t.nImpl.len > 0:
# Guard against endless recursion when a field references this type
# itself. Unlike sighashes (which temporarily clears `sfAnon`/`sfGenSym`
# on the symbol), do NOT mutate: `typeKey` runs during sem — it is
# called unconditionally from `modulegraphs.setAttachedOp` — so a
# 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):
c.treeKey(t.nImpl, flags + {CoHashTypeInsideNode}, conf)
c.visited.excl t.itemId
else:
c.m.addIdent "´empty"
# Object inheritance is part of identity: key the base class too.
if t.kind == tyObject and t.sonsImpl.len > 0 and t.sonsImpl[0] != nil:
c.typeKey t.sonsImpl[0], flags, conf
else:
c.m.addIdent "`bug"
of tyFromExpr:
@@ -238,10 +299,19 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef
c.symKey(t.nImpl[i].sym, conf)
c.typeKey(t.nImpl[i].sym.typImpl, flags+{CoIgnoreRange}, conf)
else:
for i in 1..<t.sonsImpl.len:
# ALL sons are tuple fields (son 0 included — unlike tyProc, where
# son 0 is the return type). Starting at 1 dropped the first field,
# collapsing e.g. `(PSym, NifIndexEntry)` and `(PType, NifIndexEntry)`
# onto one key, so hook lookup called the wrong `=destroy`/`=sink`
# (incompatible-argument C errors). Mirrors sighashes' `for a in t.kids`.
for i in 0..<t.sonsImpl.len:
c.typeKey t.sonsImpl[i], flags+{CoIgnoreRange}, conf
of tyRange:
if CoIgnoreRange notin flags:
if t.sonsImpl.len == 0:
# bare `range` typeclass: no base type, key the kind alone
withTree c.m, toNifTag(t.kind):
c.m.addEmpty()
elif CoIgnoreRange notin flags:
withTree c.m, toNifTag(t.kind):
c.treeKey(t.nImpl, {}, conf)
c.typeKey(t.sonsImpl[^1], flags, conf)
@@ -254,12 +324,28 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef
c.typeKey(t.skipModifierB, flags, conf)
of tyProc:
withTree c.m, (if tfIterator in t.flagsImpl: "itertype" else: "proctype"):
if CoProc in flags and t.nImpl != nil:
# Proc parameter *types* are part of the type's identity. Under IC the
# parameters live in `nImpl` (`sonsImpl` holds only the return type), so a
# loaded proc type has an empty `sonsImpl[1..]`; reading params from there
# would silently drop them and collide every same-return/same-callconv
# closure onto one key (e.g. `proc(cb: proc())` onto bare `proc()`),
# which made hook lookup resolve to the wrong `=copy`. Prefer `nImpl`
# (consistent in-memory and after load); hash param types only, not their
# symbols — parameter names do not affect type identity.
if t.nImpl != nil and t.nImpl.kind == nkFormalParams:
let params = t.nImpl
for i in 1..<params.len:
let param = params[i].sym
c.symKey(param, conf)
c.typeKey(param.typImpl, flags, conf)
if params[i].kind == nkSym:
# The param sym may be a lazily-loaded stub: force it in (as `symKey`
# does) so its type is available, then hash the param *type* only —
# parameter names are not part of the type's identity. Without the
# load the type reads back nil at codegen and the key silently loses
# its parameters (collapsing distinct closure types onto one key).
let ps = params[i].sym
if ps.state == Partial and c.sl != nil: c.sl(ps)
c.typeKey(ps.typImpl, flags, conf)
else:
c.typeKey(params[i].typField, flags, conf)
else:
for i in 1..<t.sonsImpl.len:
c.typeKey(t.sonsImpl[i], flags, conf)
@@ -270,8 +356,12 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef
if tfVarargs in t.flagsImpl: c.m.addIdent "´varargs"
of tyArray:
withTree c.m, toNifTag(t.kind):
c.typeKey(t.sonsImpl[^1], flags-{CoIgnoreRange}, conf)
c.typeKey(t.sonsImpl[0], flags-{CoIgnoreRange}, conf)
if t.sonsImpl.len == 0:
# bare `array` typeclass: no element/index types
c.m.addEmpty()
else:
c.typeKey(t.sonsImpl[^1], flags-{CoIgnoreRange}, conf)
c.typeKey(t.sonsImpl[0], flags-{CoIgnoreRange}, conf)
else:
withTree c.m, toNifTag(t.kind):
for i in 0..<t.sonsImpl.len:
@@ -280,6 +370,16 @@ proc typeKey(c: var Context; t: PType; flags: set[ConsiderFlag]; conf: ConfigRef
c.m.addIdent "´notnil"
proc typeKey*(t: PType; conf: ConfigRef; tl: TypeLoader; sl: SymLoader): string =
var c: Context = Context(m: createMangler(30, -1), tl: tl, sl: sl)
typeKey(c, t, {}, conf)
var c: Context = Context(m: createMangler(30, -1), tl: tl, sl: sl,
visited: initHashSet[ItemId]())
# Mirror the flags liftdestructors uses for its `canonTypes` hash
# (`hashType(skipped, {CoType, CoConsiderOwned, CoDistinct})`): hook keys must
# distinguish what hook *lifting* distinguishes. With empty flags a generic
# `distinct` instance (e.g. nilcheck's `SeqOfDistinct[T, U]`) took the bare
# `symKey` branch — the sym is the generic's and thus SHARED by all
# instances, so `SeqOfDistinct[I, PNode]` and `SeqOfDistinct[I, Nilability]`
# collided onto one key and hook lookup returned the wrong `=sink`
# ("incompatible type for argument" in the generated C). Under `CoDistinct` a
# `tfFromGeneric` distinct keys as sym + base type, keeping instances apart.
typeKey(c, t, {CoType, CoConsiderOwned, CoDistinct}, conf)
result = c.m.extract()

View File

@@ -1069,9 +1069,10 @@ proc sameBackendTypeIgnoreRange*(x, y: PType): bool =
c.cmp = dcEqIgnoreDistinct
result = sameTypeAux(x, y, c)
proc sameBackendTypePickyAliases*(x, y: PType): bool =
proc sameBackendTypePickyAliases*(x, y: PType, flags: TTypeCmpFlags = {}): bool =
var c = initSameTypeClosure()
c.flags.incl {IgnoreTupleFields, IgnoreRangeShallow, PickyCAliases, PickyBackendAliases}
c.flags.incl flags
c.cmp = dcEqIgnoreDistinct
result = sameTypeAux(x, y, c)

View File

@@ -185,6 +185,9 @@ proc root(v: var Partitions; start: int): int =
proc potentialMutation(v: var Partitions; s: PSym; level: int; info: TLineInfo) =
let id = variableId(v, s)
if id >= 0:
# mutated here => alive here: keep aliveEnd in sync so dangerousMutation catches
# mutations recorded after the var's last use (e.g. via a call arg). See #25595.
v.s[id].aliveEnd = max(v.s[id].aliveEnd, v.abstractTime)
let r = root(v, id)
let flags = if s.kind == skParam:
if isConstParam(s):

View File

@@ -28,7 +28,7 @@ from magicsys import getSysType
const
traceCode = defined(nimVMDebug)
when hasFFI:
when defined(nimHasLibFFI): # == hasFFI; spelled out for the IC dep scanner
import evalffi
@@ -1310,6 +1310,9 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
var a = regs[rb].node
if a.kind == nkVarTy: a = a[0]
if a.kind == nkSym:
# a macro observed this symbol's implementation: NeedsImpl edge to
# its home module under IC.
recordIcImplDep(c.graph, a.sym)
regs[ra].node = if a.sym.ast.isNil: newNode(nkNilLit)
else: copyTree(a.sym.ast)
regs[ra].node.flags.incl nfIsRef
@@ -1319,6 +1322,7 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
decodeB(rkNode)
let a = regs[rb].node
if a.kind == nkSym:
recordIcImplDep(c.graph, a.sym)
regs[ra].node =
if a.sym.ast.isNil:
newNode(nkNilLit)
@@ -1951,7 +1955,21 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
if regs[rb].node.kind != nkSym:
stackTrace(c, tos, pc, "node is not a symbol")
else:
regs[ra].node.strVal = $sigHash(regs[rb].node.sym, c.config)
let shSym = regs[rb].node.sym
# When `signatureHash` is applied to a type (e.g. a `T: typedesc`/generic
# param), hash the *type* it denotes, not the parameter symbol. Hashing the
# symbol routes through `hashNonProc`, which mixes in `s.disamb` — a
# per-module instantiation counter. Under incremental compilation the
# registering module and a consuming module instantiate the surrounding
# generic separately, get different `disamb`s, and produce different
# hashes for the same type (nim-serialization's auto-serialization lookup
# missed because of this). Hashing the underlying type via `hashType` is
# type-identity based and stable across the NIF boundary.
let shTyp = shSym.typ
if shTyp != nil and shTyp.kind == tyTypeDesc and shTyp.hasElementType:
regs[ra].node.strVal = $hashType(shTyp.elementType, c.config)
else:
regs[ra].node.strVal = $sigHash(shSym, c.config)
of opcSlurp:
decodeB(rkNode)
createStr regs[ra]

View File

@@ -308,7 +308,7 @@ proc newCtx*(module: PSym; cache: IdentCache; g: ModuleGraph; idgen: IdGenerator
callDepth: g.config.maxCallDepthVM,
comesFromHeuristic: unknownLineInfo, callbacks: @[], callbackIndex: initTable[string, int](), errorFlag: "",
cache: cache, config: g.config, graph: g, idgen: idgen,
contstantTab: initNodeTable(true))
contstantTab: initNodeTable(true), templInstCounter: new int)
proc refresh*(c: PCtx, module: PSym; idgen: IdGenerator) =
c.module = module

View File

@@ -36,7 +36,7 @@ import
magicsys, options, lowerings, lineinfos, transf, astmsgs,
treetab
from modulegraphs import getBody
from modulegraphs import getBody, recordIcImplDep
when defined(nimCompilerStacktraceHints):
import std/stackframes
@@ -46,7 +46,7 @@ const
when debugEchoCode:
import std/private/asciitables
when hasFFI:
when defined(nimHasLibFFI): # == hasFFI; spelled out for the IC dep scanner
import evalffi
type
@@ -786,8 +786,12 @@ proc genBinaryABCD(c: PCtx; n: PNode; dest: var TDest; opc: TOpcode) =
c.freeTemp(tmp2)
c.freeTemp(tmp3)
template sizeOfLikeMsg(name): string =
"'$1' requires '.importc' types to be '.completeStruct'" % [name]
template sizeOfLikeMsg(name, incompleteStruct): string =
block:
if incompleteStruct:
"'$1' cannot be used with '.incompleteStruct' types" % [name]
else:
"'$1' requires '.importc' types to be '.completeStruct'" % [name]
proc genNarrow(c: PCtx; n: PNode; dest: TDest) =
let t = skipTypes(n.typ, abstractVar-{tyTypeDesc})
@@ -1476,11 +1480,14 @@ proc genMagic(c: PCtx; n: PNode; dest: var TDest; flags: TGenFlags = {}, m: TMag
else:
globalError(c.config, n.info, "expandToAst requires a call expression")
of mSizeOf:
globalError(c.config, n.info, sizeOfLikeMsg("sizeof"))
let arg = n[1].typ.skipTypes({tyTypeDesc})
globalError(c.config, n.info, sizeOfLikeMsg("sizeof", tfIncompleteStruct in arg.flags))
of mAlignOf:
globalError(c.config, n.info, sizeOfLikeMsg("alignof"))
let arg = n[1].typ.skipTypes({tyTypeDesc})
globalError(c.config, n.info, sizeOfLikeMsg("alignof", tfIncompleteStruct in arg.flags))
of mOffsetOf:
globalError(c.config, n.info, sizeOfLikeMsg("offsetof"))
let arg = n[1].typ.skipTypes({tyTypeDesc})
globalError(c.config, n.info, sizeOfLikeMsg("offsetof", tfIncompleteStruct in arg.flags))
of mRunnableExamples:
discard "just ignore any call to runnableExamples"
of mDestroy, mTrace: discard "ignore calls to the default destructor"
@@ -1775,8 +1782,15 @@ proc genGlobalInit(c: PCtx; n: PNode; s: PSym) =
# This is rather hard to support, due to the laziness of the VM code
# generator. See tests/compile/tmacro2 for why this is necessary:
# var decls{.compileTime.}: seq[NimNode] = @[]
# Load the slot's ADDRESS (not its value): the lazy initializer must REPLACE
# the null slot, which `opcWrDeref` only does for an `rkNodeAddr` target
# (`nAddr[] = n` for refs). With `opcLdGlobal` the slot value is loaded and for
# a ref-typed global that value is an `nkNilLit` ("nil ref"); writing through it
# hits the VM's nil-deref guard ("attempt to access a nil address"). This path
# is reached for compile-time globals whose defining module is restored from a
# NIF under `nim ic` (so `setupCompileTimeVar` never ran to eagerly init them).
let dest = c.getTemp(s.typ)
c.gABx(n, opcLdGlobal, dest, s.position)
c.gABx(n, opcLdGlobalAddr, dest, s.position)
if s.astdef != nil:
let tmp = c.genx(s.astdef)
c.genAdditionalCopy(n, opcWrDeref, dest, 0, tmp)
@@ -1842,6 +1856,8 @@ proc genArrAccessOpcode(c: PCtx; n: PNode; dest: var TDest; opc: TOpcode;
if dest < 0: dest = c.getTemp(n.typ)
if opc in {opcLdArrAddr, opcLdStrIdxAddr} and gfNodeAddr in flags:
c.gABC(n, opc, dest, a, b)
if c.prc.regInfo[a].kind >= slotTempUnknown:
c.prc.regInfo[a].kind = slotTempPerm
elif needsRegLoad():
var cc = c.getTemp(n.typ)
c.gABC(n, opc, cc, a, b)
@@ -1858,6 +1874,8 @@ proc genObjAccessAux(c: PCtx; n: PNode; a, b: int, dest: var TDest; flags: TGenF
if dest < 0: dest = c.getTemp(n.typ)
if {gfNodeAddr} * flags != {}:
c.gABC(n, opcLdObjAddr, dest, a, b)
if a < c.prc.regInfo.len and c.prc.regInfo[a].kind >= slotTempUnknown:
c.prc.regInfo[a].kind = slotTempPerm
elif needsRegLoad():
var cc = c.getTemp(n.typ)
c.gABC(n, opcLdObj, cc, a, b)
@@ -2456,6 +2474,10 @@ proc optimizeJumps(c: PCtx; start: int) =
proc genProc(c: PCtx; s: PSym): VmProcInfo =
result = c.procToCodePos.getOrDefault(s.id, NoVmProcInfo)
if result.usedRegisters < 0:
# compile-time execution consumes this routine's BODY: under IC that is a
# NeedsImpl dependency on the routine's home module (iface-cookie gating
# alone would miss body-only edits, e.g. `const x = dep.foo()`).
recordIcImplDep(c.graph, s)
#if s.name.s == "outterMacro" or s.name.s == "innerProc":
# echo "GENERATING CODE FOR ", s.name.s
let last = c.code.len-1
@@ -2469,7 +2491,9 @@ proc genProc(c: PCtx; s: PSym): VmProcInfo =
c.procToCodePos[s.id] = result
# thanks to the jmp we can add top level statements easily and also nest
# procs easily:
inc c.graph.inVMTransform
let body = transformBody(c.graph, c.idgen, s, if isCompileTimeProc(s): {} else: {useCache})
dec c.graph.inVMTransform
let procStart = c.xjmp(body, opcJmp, 0)
var p = PProc(blocks: @[], sym: s)
let oldPrc = c.prc

View File

@@ -36,7 +36,9 @@ from std/osproc import nil
when defined(nimPreviewSlimSystem):
import std/syncio
else:
when not defined(nimPreviewSlimSystem):
# explicit negated `when` rather than `else:` so nifler's dep scanner guards
# this import with its condition (it emits `else:` imports unconditionally).
from std/formatfloat import addFloatRoundtrip, addFloatSprintf

View File

@@ -152,6 +152,8 @@ proc sortVTableDispatchers*(g: ModuleGraph) =
rootItemIdCount.inc(baseType.itemId)
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]
for baseType in rootTypeSeq:

View File

@@ -168,6 +168,19 @@ nimblepath="$home/.nimble/pkgs/"
switch_gcc.cpp.options.always = "-g -Wall -O2 -ffunction-sections -march=armv8-a -mtune=cortex-a57 -mtp=soft -fPIE -D__SWITCH__ -fno-rtti -fno-exceptions -std=gnu++11"
@end
# Emscripten toolchain for WebAssembly (wasm32, or wasm64/Memory64).
@if emscripten:
cc = clang
clang.exe = "emcc"
clang.linkerexe = "emcc"
clang.cpp.exe = "emcc"
clang.cpp.linkerexe = "emcc"
@if wasm64:
passC = "-sMEMORY64=1"
passL = "-sMEMORY64=1"
@end
@end
# Configuration for the Intel C/C++ compiler:
@if windows:
icl.options.speed = "/Ox /arch:SSE2"

View File

@@ -188,7 +188,7 @@ objectPart = IND{>} objectPart^+IND{=} DED
/ objectWhen / objectCase / 'nil' / 'discard' / declColonEquals
objectDecl = 'object' ('of' typeDesc)? COMMENT? objectPart
conceptParam = ('var' | 'out' | 'ptr' | 'ref' | 'static' | 'type')? symbol
conceptDecl = 'concept' conceptParam ^* ',' (pragma)? ('of' typeDesc ^* ',')?
conceptDecl = 'concept' (conceptParam ^* ',' (pragma)?)? ('of' typeDesc ^* ',')?
&IND{>} stmt
typeDef = identVisDot genericParamList? pragma '=' optInd typeDefValue
indAndComment?

412
doc/ic.md
View File

@@ -2,165 +2,325 @@
Incremental Compilation (IC)
======================================
The ``nim ic`` command provides incremental compilation support for Nim projects,
allowing faster rebuilds by reusing previously compiled intermediate representations
of modules that haven't changed.
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.
This document describes **how `nim 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
`nifmake`-driven per-module rules (see *The backend*).
Overview
========
Incremental compilation works by decomposing the compilation process into several stages:
The pipeline has two halves driven by one process (`nim ic`, `commandIc` in
``compiler/deps.nim``) that constructs a dependency graph, writes a build file,
and hands it to ``nifmake``:
1. **Parsing** - Source files are parsed into an abstract syntax tree (AST)
2. **Semantic Analysis** - Symbols are resolved and type checking is performed
3. **Code Generation** - Platform-specific code is generated from the analyzed AST
4. **Linking** - The generated code is linked into an executable
1. **Frontend** — per module:
- ``nifler parse --deps`` turns ``.nim`` source into a parsed NIF
(``.p.nif``) plus a static dependency list (``.deps.nif``).
- ``nim m`` (the *semantic* step, `cmdM`) reads the parsed NIF + the
precompiled NIFs of the module's imports, type-checks, and writes the
**semmed NIF** (``.nif``) plus invalidation sidecars (see *Cookies*).
2. **Backend** — ``nim nifc`` (`cmdNifC`, ``compiler/nifbackend.nim``) reads the
semmed NIFs, generates C, compiles and links.
The IC mechanism caches the results of earlier stages in NIF files
(Nim intermediate format): ``.p.nif`` (parsed), ``.deps.nif`` (dependencies),
and ``.nif`` (semantically analyzed). When recompiling, only modules that have
changed need to be reprocessed through the semantic analysis and code generation
stages, significantly reducing compilation time for large projects.
``nifmake`` orders the steps by their input/output files: every `nim m` runs
before the `nim nifc` step that consumes its NIF, and a step re-fires only when
one of its inputs is newer than its outputs. The driver invokes ``nifmake run
--parallel`` by default, so independent steps at the same DAG depth fan out
across cores; pass ``-d:icNoParallel`` to serialize (readable child output when
debugging a build).
NIF File Format
===============
Artifacts (the NIF zoo)
=======================
NIF (Nim Intermediate Format) files are text-based files that use a Lisp-like
syntax. They employ a hybrid format where byte offsets into the text are used for
efficient access, making them simultaneously human-readable and machine-efficient.
The text representation is particularly valuable for debugging and introspection.
Per module ``<suffix>`` (a content hash of the path; see *NIF symbols* below),
under the nimcache directory:
Each ``.nim`` module produces its own ``.nif`` file during compilation.
The NIF format contains:
| File | Producer | Purpose |
| ---- | -------- | ------- |
| ``<s>.p.nif`` | nifler | parsed AST (syntactic) |
| ``<s>.deps.nif`` | nifler | **static** import list (syntactic `import`s) |
| ``<s>.s.deps.nif`` | `nim m` | **real** post-sem imports (incl. macro-generated); see *Discovery* |
| ``<s>.nif`` | `nim m` | semmed module (symbols resolved, typed) |
| ``<s>.iface.nif`` | `nim m` | **iface cookie**: hash of the importer-visible surface |
| ``<s>.impl.nif`` | `nim m` | **impl cookie**: hash of the entire content (bodies included) |
| ``<s>.edges.nif`` | `nim m` | **NeedsImpl edges**: modules whose bodies this sem consumed |
| ``<s>.c.nif`` | `nim nifc` | the C text as a NIF, with def/ref markers for DCE & dedup |
| ``ic_config.cfg.nif`` | driver | precompiled config replayed by every child (`icconfig.nim`) |
| ``ic.version`` | driver | format stamp; a mismatch wipes the cache (`icFormatVersion`) |
- **Header** - Version information (e.g., `(.nif27)`)
- **Dependencies** - List of source files and dependencies
- **Interface** - Exported symbols and their indices
- **Body** - The intermediate representation of the module's code in Lisp-like syntax
The NIF format is designed specifically for Nim and allows efficient serialization
and deserialization of the compiler's intermediate representation while remaining
readable and debuggable by tools and developers.
The ``nim ic`` Switch
=====================
The ``nim ic`` command initiates incremental compilation for a project.
It automatically manages the build process by:
1. Parsing all source files into ``.nif`` format (using the ``nifler`` tool)
2. Performing semantic analysis on modified modules
3. Generating code only for modules with changes or dependencies on changed modules
4. Generating a build file (in NIFMake format) that orchestrates the compilation
5. Executing the build file through ``nifmake``
Prerequisites
-------------
- **nifler** - Tool for parsing Nim source files into NIF format. The ``nim ic`` command uses ``nifler parse --deps`` to generate both parsed files (``.p.nif``) and dependency files (``.deps.nif``).
- **nifmake** - Build orchestration tool that follows dependencies and executes the build rules defined in ``.build.nif`` files.
If these tools are not available, ``nim ic`` will display instructions on how to
obtain them.
Key Modules for IC Logic
NIF symbols and ownership
=========================
The primary modules in the compiler that handle incremental compilation logic are:
(See ``../nifspec/doc/nif-spec.md``.) A global symbol is
``<ident>.<disamb>.<moduleSuffix>``. For a **generic instantiation** the
`<disamb>` is not a counter but a *content hash* — `setInstanceDisamb`
(``modulegraphs.nim``) MD5s the generic's identity plus the `typeKey` of every
concrete type argument, masks it to 30 bits and tags it with `InstanceDisambBit`.
So the only part of the name that varies between two modules making the **same**
instantiation (`seq[Foo]`) is the `<moduleSuffix>`. Two consequences drive the
backend:
- **deps.nim** - Dependency analysis and build file generation. Contains the
``commandIc`` procedure which is the main entry point for the ``nim ic`` command.
This module orchestrates the incremental compilation process, handling dependency
traversal (via ``nifler deps``), build rule generation, and build file creation.
The build file is written to ``nifcache/`` directory. This module also explicitly
models ``system.nim`` as a dependency of all modules.
- **Instance names are content-addressed**: the same instantiation produced in
different modules yields the *same* `<ident>.<disamb>`, so a deterministic dedup
is possible by the *module-suffix-stripped* name. The cross-TU C name
(`ccgtypes.sharedInstanceCName`) and the **merge** stage's live-set/owner
decision (`nifbackend.computeMergeDecision`) both key on this stripped form.
- **The suffix names a mint-site owner.** The `<moduleSuffix>` is the module
*that minted the instance* (the instantiation site), so the same instance has a
different full name in each module that makes it. Because every `cg` process
emits the instances it demands (*emit-everywhere*), the same definition can be
produced by several translation units; the **merge** stage then deterministically
picks the single artifact allowed to embed each body (smallest claimant), which
is the cross-process replacement for the old in-process single-writer machinery.
- **ast2nif.nim** - Core mapping between AST and NIF.
The driver: graph construction (`commandIc`)
============================================
1. Stamp/wipe the cache by ``icFormatVersion``.
2. Seed the graph with the root module and **`system.nim`**. `system`'s entire
import closure is folded into one node (one `nim m` invocation) — see
*single-writer* below.
3. ``traverseDeps`` runs ``nifler`` per module and reads ``.deps.nif`` to add
import edges.
4. **SCC grouping**: strongly-connected import cycles are collapsed (Tarjan).
A singleton compiles as ``nim m <mod>``; a cycle compiles as one
``nim m <rep> --icGroup:<member>…`` that builds every member *from source* in
one process (resolving the recursion in memory) and writes each member's NIF.
Only edges *leaving* the component become build-graph inputs.
5. **Discovery fixpoint**: write the build file, run ``nifmake``; if it fails,
re-derive the graph from every module's ``.s.deps.nif`` (adding nodes/edges
for imports the static scanner missed), and retry. See *Discovery*.
6. The backend step (`nim nifc`) depends on every module's semmed NIF, so
``nifmake`` runs it last.
**Code, Logic & Debugging**
===========================
Invalidation: the cookie system
================================
This section focuses on the compiler-side code paths, the logic you will
inspect while debugging IC, and a pragmatic manual workflow for bug hunting
using local invocations such as ``nim m --nimcache:nifcache``.
A dependent must re-sem only when a dependency's relevant surface changed. Two
hashes per module (``ast2nif.nim``):
Core places to inspect
- **`compiler/deps.nim`**: generates the NIF-based build file and implements
``commandIc`` (entry point for ``nim ic``). Look for how build rules are
emitted (calls to the NIF builder) and how inputs/outputs are wired.
- **`compiler/modulegraphs.nim`** and **`compiler/pipelines.nim`**:
dependency graph and compilation pipeline integration — useful when a module
is rebuilt unexpectedly.
- **iface cookie** (``.iface.nif``): hashes only the *importer-visible* surface —
exported declarations' **signatures** (for *all* routine kinds: plain procs,
templates, macros, generics, `inline` procs alike), full content for
consts/types, plus import/export/replay/hook records. Routine **bodies are
excluded.** It also chains in the iface cookies of its own dependencies, so a
surface change anywhere in the import closure propagates. A `nim m` rule for a
module depends on its dependencies' iface cookies, so a body-only edit moves no
iface cookie and stops the re-sem cascade.
- **impl cookie** (``.impl.nif``): hashes the *entire* serialized content (private
defs and bodies included), with the module's own iface mixed in.
Understanding the NIF text
- NIF files are human-readable; open the per-module ``.nif`` files in
``nifcache/`` to inspect parsed ASTs, dependency lists and interface tables.
- Because NIF uses textual nodes and byte offsets, tools can quickly seek to
positions in the file — but for debugging you usually only need to read the
file top-to-bottom.
**NeedsImpl edges** (``.edges.nif``): if a module *consumed another module's body*
during sem — a macro expansion, a generic instantiation, a `getImpl`, or a
compile-time call run in the VM — it records a strong edge. The dependent is then
gated on that dependency's **impl** cookie instead of its iface cookie, so e.g.
`const x = dep.foo()` re-sems when `foo`'s body changes. Recording sites:
`semExprs.semTemplateExpr` (templates), `seminst.generateInstance` (generics),
`vmgen.genProc` (VM/macros/CT procs), `vm.opcGetImpl` (`getImpl`). Inline
iterators and `inline` procs are *not* tracked — they are inlined at codegen,
where the backend's NIF-mtime invalidation re-codegens their users.
Manual bug-hunting workflow
- Prepare a clean nimcache directory (relative to your project):
Discovery of macro-generated imports
====================================
```bash
mkdir -p nifcache
```
The static scanner only sees syntactic `import`s. A macro can synthesize one
(chronicles does `parseStmt("import chronicles/textlines")` driven by the
`chronicles_sinks` define). Such an import is invisible until sem runs the macro.
Each `nim m` records the imports it *actually* resolved (via the
``semdata.addImportFileDep`` hook → ``graph.importDeps`` → ``ast2nif.writeSemDeps``)
into ``<s>.s.deps.nif``; a child that fails on a not-yet-built import flushes it
before erroring. The driver re-derives the graph from those sidecars — adding the
missing node + the importer→import edge — and reruns to a fixpoint. (This replaced
an earlier `icmissing.txt` side channel.)
- Parse/semantic-check a single module and write NIF/sem artifacts:
The backend: per-module `nifc` stages
=====================================
```bash
nim m --nimcache:nifcache path/to/module.nim
```
Codegen is no longer one whole-program process. ``nim nifc`` (`cmdNifC`,
``compiler/nifbackend.nim``) is invoked once per **stage** via
``--icBackendStage:<stage>``; `commandIc` emits these as ordinary `nifmake` rules
so "which TUs rebuild" is just "which rules `nifmake` re-fires from input mtimes"
— exactly as the frontend already works. There are four stages:
- ``nim m`` runs the compiler up to the semantic checking stage for the
specified module and emits intermediate cache files into ``nifcache/``.
- Use this to reproduce and isolate failures in the semantic stage.
1. **`cg`** (``--icBackendStage:cg --icBackendModule:<suffix>``) — generate C for
the *single* named module and write only its ``<s>.c.nif`` artifact. A non-main
target loads only its own import closure (`loadDepClosure`), so the whole
program is **not** pulled into every parallel `cg` process. Codegen is still
demand-driven and **emit-everywhere**: a `cg` process emits every entity it
demands (generic instances, hooks, RTTI), referencing nothing `extern`-only.
There is no whole-program DCE here — a liveness pass over all ~260 NIFs would
cost ~900 MB for a result the merge stage recomputes anyway. The **main**
module's `cg` is special: it loads everything (`loadBackendModules`), emits the
whole-program method dispatchers and `NimMain`, and registers every other
module's init/datInit from the `.c.nif` meta heads — so it runs *last*, after
every other ``.c.nif`` exists. Every `cg` rule always leaves a ``.c.nif`` (empty
if the module owns no code) so its nifmake output exists and the rule settles.
2. **`merge`** (``--icBackendStage:merge``) — a pure artifact pass, *no module
graph loaded*. Reads every ``.c.nif``, computes the one program-wide live set
and, for each unique definition that several `cg` processes emitted, the single
artifact allowed to embed its body; writes that to a merge-decision file
(`computeMergeDecision` / `writeMergeDecision`). This is the cross-process
replacement for the old in-process first-claimant + DCE coordination.
3. **`emit`** (``--icBackendStage:emit --icBackendModule:<suffix>``) — render the
target module's final ``.c`` from its ``.c.nif`` and the merge decision
(`renderCFromArtifact`, dropping globally-dead and non-owned bodies). No codegen
runs; the target is loaded only so `getCFile` yields the path `cg` wrote.
4. **`link`** (``--icBackendStage:link``) — register every module's emitted ``.c``
and run `extccomp.callCCompiler` once (it parallelizes per-file cc and skips
up-to-date objects). Per-module C compile/link directives (`{.passL.}` etc.) are
re-collected here via `replayBackendActions`, since the `cg` processes that
originally saw them are separate processes (without this, e.g. `math`'s `-lm`
would be lost → undefined `floor`/`pow` at link).
- Inspect the generated files for that module under ``nifcache/`` (look for
``.nif``, sem/parsed artifacts). Because NIF is text-based you can open and
grep it directly:
Because each stage is a `nifmake` rule keyed on file mtimes, a body-only edit to
one module re-fires that module's `cg`+`emit` (and the `merge`/`link`), not the
whole program — and an unchanged module's `cg` does not run at all.
```bash
sed -n '1,200p' nifcache/ModuleName.nif
grep -n "someSymbol" -n nifcache/ModuleName.nif
```
Edge cases (and why the machinery exists)
=========================================
- To reproduce a full incremental compilation of the project, generate the
build file and run it (``nim ic`` automates this). The build file is generated
in ``nifcache/`` directory. To debug an individual build step, run the command
that the build file would execute manually:
- Parsing step: ``nifler parse --deps input.nim`` (produces ``.p.nif`` and ``.deps.nif``)
- Semantic step: ``nim m --nimcache:nifcache input.nim`` (produces ``.nif``)
- Code generation: ``nim nifc --nimcache:nifcache input.nim`` (produces executable)
- **Single-writer.** Instance type-ids are minted in process-local order, so if
two `nim m` processes both write a module's NIF (e.g. a stdlib module pulled
into `system`'s from-source closure *and* given its own rule), the second
overwrites with different ids and every module checked against the first carries
dangling refs ("symbol has no offset"). Fixed by folding `system`'s closure into
one SCC and by **forwarding the project's defines** to every child so their
`when` bodies (hence import sets and NIF contents) match the scanner's.
- **`when … else: import`.** nifler emits `else`-branch imports unguarded, so a
dead `else: import` would be scheduled. The compiler's own sources were rewritten
to explicit negated `when`s; the vendored nifler later learned to negate prior
conditions for the `else`.
- **`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).
- **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
fixed-point check). It writes its binary to ``bin/nim_ic`` and never clobbers
``bin/nim``.
- Force a cache invalidation for a single module by removing its NIF/sem
artifact and re-running the semantic step:
Resolved by the rewrite
-----------------------
```bash
rm nifcache/ModuleName.nif
nim m --nimcache:nifcache path/to/ModuleName.nim
```
The whole-program backend's hand-rolled mini-`nifmake` — `computeModuleReuse`,
`enforceDefRetention`, `redirectToLiveModule`, the cached-defs/claim bookkeeping
and the standalone `dce.nim` — **is gone**. Reuse is now just per-rule `nifmake`
mtime checks, and the single-writer decision is the `merge` stage. The old
**cross-mm / `--force` `var not init`** hazard dissolved with it: every codegen
rule's config (including `--mm`) is a declared `nifmake` input, so a stale-config
TU is simply rebuilt rather than mixed in. `koch bootic` is green under both `orc`
and `--mm:refc`.
- When investigating incorrect replayed state (pragmas, `{.compile: ...}`):
inspect the replay actions in ``compiler/ic/replayer.nim`` and open the
module's NIF to find the ``toReplay``/action entries that will be executed
during reload.
Known residual hack
-------------------
Tips for efficient debugging
- Use ``--path:...`` flags when invoking ``nim m`` to emulate the exact
search paths used in your project, e.g. ``--path:lib --path:vendor``.
- Compare two successive ``.nif`` files with ``diff`` to see what changed and
why a module was rebuilt.
- `deps.runNifler` still uses `setLastModificationTime` to mark its scan
up-to-date and deletes a stale parsed file to coordinate with the nifmake nifler
rule — the driver duplicating nifmake's freshness logic. It is explicitly
flagged in the source and folds away with a full frontend/nifler split.
Where to change behavior
- Cache invalidation decisions and build-rule emission are implemented in
``compiler/deps.nim``. When investigating surprising
rebuilds, instrument those modules to log the footprint/hash/comparison
outcome.
Status and performance
======================
`nim 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
case, since incremental reuse is not exercised):
| | wall | notes |
| - | ---- | ----- |
| `koch boot` (classic) | ~1m00s | reference |
| `koch bootic` (`nim ic`) | ~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
many-core box that overhead is absorbed by the parallel `nim m`/`nifc` fan-out,
and the C compile+link floor is shared with the classic backend. On few-core
machines the cold gap is correspondingly wider — IC trades single-build latency
for incremental latency.
The cold number is the *least* favourable comparison: it pays IC's full per-process
overhead while using none of its incremental machinery. **Warm rebuilds — the
actual point of IC — recompile only the modules whose inputs changed** (a body-only
edit re-fires one module's `cg`+`emit`, not the program), so an edit-driven rebuild
is a small fraction of either full build.
The strategic direction (decided 2026-06-13) is to make this NIF backend
(`cmdNifC`) the **default** code generator. The per-module pipeline above is the
realization of that direction; remaining work is *promotion + deletion* of the
classic path, not new machinery.
Design notes and open decisions
===============================
The per-module backend (above) mirrors Nimony's ``src/nimony/deps.nim``: the
backend stopped re-implementing `nifmake`; each stage is a build rule, so reuse is
just mtime checks and the merge stage is the only cross-module coordination.
Settled vs. open:
- **Ownership.** Emittable entities (generic instances, type-bound hooks, RTTI,
lifted procs) are emit-everywhere at `cg` time and deduplicated at `merge` time
(smallest claimant owns each unique body). The earlier idea of a *static*
per-suffix owner computed before codegen was not needed — content-addressed names
make the merge decision deterministic. The precise owner *rule* (minting module
vs. root-type's module) can still be tuned where it would force a downstream
package to own stdlib code.
- **Remaining cleanup.** The `runNifler` `setLastModificationTime` coordination
(above) folds away with a full frontend/nifler split; dead `when` imports could
also be pruned during the `.s.deps` re-derivation.
Validation bar (held on every change): `koch bootic` must reach its byte-identical
fixed point, and binary size must not regress (DCE parity), across the
external-package CI set.
Code, logic & debugging
========================
Core modules:
- **`compiler/deps.nim`** — graph construction, SCC grouping, discovery fixpoint,
build-file generation; `commandIc`.
- **`compiler/ast2nif.nim`** — AST↔NIF, the cookie hashes (`cookieSd`,
`writeIfaceCookie`, `writeImplCookie`, `writeEdgesFile`, `writeSemDeps`).
- **`compiler/nifbackend.nim`** — the per-module backend stages (`generateCgStage`,
`generateMergeStage`, `generateEmitStage`, `generateLinkStage`).
- **`compiler/cnif.nim`** — `.c.nif` artifact read/write, `computeMergeDecision`,
`renderCFromArtifact`.
- **`compiler/icconfig.nim`** — precompiled config.
- **`compiler/pipelines.nim`** / **`modulegraphs.nim`** — pipeline integration and
the graph state (`importDeps`, `icImplDeps`, `icCnifFiles`, `instDisambs`, …).
Manual workflow:
- Frontend a module: ``nim m --nimcache:nifcache path/to/mod.nim`` (writes
``.nif`` + cookies + ``.s.deps``).
- Backend is stage-based (a bare ``nim nifc main.nim`` errors — there is no
whole-program fallback). The exact per-stage commands `nifmake` runs are in the
``*.backend.build.nif`` build file; rerun one directly against an existing cache,
e.g. ``nim nifc --nimcache:nifcache --icBackendStage:cg --icBackendModule:<suffix> main.nim``
to regenerate one module's ``.c.nif``, then ``--icBackendStage:merge`` /
``:emit`` / ``:link``.
- NIF and ``.c.nif`` files are text — open/grep them directly; ``diff`` two
successive ``.nif`` to see why a module rebuilt.
- Force a re-sem: delete the module's ``.nif`` and rerun `nim m`.
- A stale-cache crash after editing the serialization layout means bumping
``icFormatVersion`` (`compiler/options.nim`).
See also
========
- `nif-spec` - NIF format specification (text format and node grammar):
[nifspec/doc/nif-spec.md](../nifspec/doc/nif-spec.md)
- NIF format spec: [nifspec/doc/nif-spec.md](../nifspec/doc/nif-spec.md)
- NIFC (C-like target) spec: dist/nimony/doc/nifc-spec.md

View File

@@ -596,12 +596,12 @@ Regular expressions
* [re](re.html)
Procedures and operators for handling regular
expressions. The current implementation uses PCRE2.
expressions. The current implementation uses PCRE.
* [nre](nre.html)
Many help functions for handling regular expressions.
The current implementation uses PCRE2.
The current implementation uses PCRE.
Database support
----------------
@@ -661,9 +661,6 @@ Regular expressions
* [pcre](pcre.html)
Wrapper for the PCRE library.
* [pcre2](pcre2.html)
Wrapper for the PCRE2 library.
Database support
----------------

View File

@@ -6123,40 +6123,48 @@ instantiations cross multiple different modules:
```nim
# module A
type O* = object
proc genericA*[T](x: T) =
mixin init
init(x)
```
```nim
# module C
import A
proc init*(x: O) = discard
```
```nim
import C
# module B
import A, C
proc genericB*[T](x: T) =
# Without the `bind init` statement C's init proc is
# not available when `genericB` is instantiated:
# Without the `bind init` statement, C's `init` proc is not
# available when `genericA` is instantiated through `genericB`
# from `module main`, which does not import C:
bind init
genericA(x)
```
```nim
# module C
type O = object
proc init*(x: var O) = discard
```
```nim
# module main
import B, C
import A, B
genericB O()
genericB(O())
```
In module B has an `init` proc from module C in its scope that is not
taken into account when `genericB` is instantiated which leads to the
instantiation of `genericA`. The solution is to `forward`:idx: these
symbols by a `bind` statement inside `genericB`.
Because `genericA` uses `mixin init`, `init` is an open symbol that is
resolved when `genericA` is instantiated. Here `genericA` is instantiated
through `genericB`, whose final instantiation happens in `module main`.
Since `module main` does not import `module C`, `init` is not in scope at
that point, and the instantiation fails with ``undeclared identifier: 'init'``.
The `bind init` statement inside `genericB` forwards the `init` symbol that
is visible in `module B` into the instantiation of `genericA`, which makes
the example compile. This `bind`, which re-exposes a symbol to a nested
generic instantiation, is a `delegating bind`:idx:.
Templates
@@ -7996,6 +8004,9 @@ underlying C `struct`:c: in a `sizeof` expression:
pure, incompleteStruct.} = object
```
Attempting to use `sizeof` on an `incompleteStruct` type at compile-time
will error with "'sizeof' cannot be used with '.incompleteStruct' types".
CompleteStruct pragma
---------------------

View File

@@ -86,7 +86,7 @@ That means you can always use only 1 such an option with logical OR, e.g.
Meaning of `^`:literal: and `$`:literal:
========================================
`nimgrep`:cmd: PCRE2 engine is run in a single-line mode so
`nimgrep`:cmd: PCRE engine is run in a single-line mode so
`^`:literal: matches the beginning of whole input *file* and
`$`:literal: matches the end of *file* (or whole input *string* for
options like `--filename`).
@@ -97,7 +97,7 @@ Add the `(?m)`:literal: modifier to the beginning of your pattern for
Examples
========
All examples below use default PCRE2 Regex patterns:
All examples below use default PCRE Regex patterns:
+ To search recursively in Nim files using style-insensitive identifiers:

View File

@@ -16,11 +16,11 @@ const
ChecksumsStableCommit = "0b8e46379c5bc1bf73d8b3011908389c60fb9b98" # 2.0.1
SatStableCommit = "e63eaea8baf00bed8bcd5a29ffd8823abb265b39"
NimonyStableCommit = "750aa47f2139fe5ad69f04b44428b752011fe873" # unversioned \
NimonyStableCommit = "5fa72628a6867f8ca09f8955a493749cf65f006a" # 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-05-05
# Commit from 2026-06-14
# examples of possible values for fusion: #head, #ea82b54, 1.2.3
FusionStableHash = "#562467452b32cb7a97410ea177f083e6d8405734"
@@ -76,6 +76,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`)
distrohelper [bindir] helper for distro packagers
tools builds Nim related tools
toolsNoExternal builds Nim related tools (except external tools,
@@ -406,6 +407,55 @@ proc boot(args: string, skipIntegrityCheck: bool) =
if not skipIntegrityCheck:
echo "[Warning] executables are still not equal"
proc bootic(args: string, skipIntegrityCheck: bool) =
## Like `boot`, but bootstraps the compiler through the NIF-based incremental
## compiler (`nim ic`) instead of `nim c`. Differences from `boot`:
## * It starts from an already-bootstrapped Nim (found via `findStartNim`): the
## csources compiler is far too old to provide the `ic` command, and the
## `-d:nimKochBootstrap` define used by `boot`'s first stage *disables*
## `commandIc`, so neither can be used here.
## * `nim ic` drives the per-module build and the final link itself (via
## `nifmake`), so there is no `--compileOnly` + `jsonscript` split.
## The 3-step fixed-point check is kept: a successful run proves the compiler
## can compile itself under IC and reproduces a stable binary.
var output = "compiler" / "nim".exe
# Deliberately NOT `bin/nim`: `bootic` must not clobber the development
# compiler (that would replace a fast release `bin/nim` with bootic's build
# and slow every later `koch`/`nim` invocation). The IC-bootstrapped binary
# lands at `bin/nim_ic` instead; `bin/nim` is only ever read (via findStartNim).
var finalDest = "bin" / "nim_ic".exe
let smartNimcache = (if "release" in args or "danger" in args: "nimcache/ric_" else: "nimcache/dic_") &
hostOS & "_" & hostCPU
bundleChecksums(false)
let nimStart = findStartNim().quoteShell()
let times = 2 - ord(skipIntegrityCheck)
# `boot` shares the `compiler/nim` output path; remove it so a fully warm
# cache still relinks and iteration 1 cannot adopt a stale foreign binary.
removeFile output
for i in 0..times:
echo "iteration: ", i+1
# Iteration 1 may build incrementally (that's the point of IC), but every
# later iteration must start from a clean cache: with a warm cache a
# no-change rerun correctly rebuilds nothing, so iteration i+1 would just
# keep iteration i's binary and the fixed-point check would be vacuous.
# The check is only meaningful if the freshly built compiler re-translates
# everything.
if i > 0: removeDir smartNimcache
let nimi = if i == 0: nimStart else: i.thVersion
exec "$# ic --nimcache:$# $# compiler" / "nim.nim" %
[nimi, smartNimcache, args]
if sameFileContent(output, i.thVersion):
copyExe(output, finalDest)
echo "executables are equal: SUCCESS! (IC-bootstrapped compiler: ", finalDest, ")"
return
copyExe(output, (i+1).thVersion)
copyExe(output, finalDest)
when not defined(windows):
if not skipIntegrityCheck:
echo "[Warning] executables are still not equal"
# -------------- clean --------------------------------------------------------
const
@@ -550,19 +600,39 @@ proc xtemp(cmd: string) =
finally:
copyExe(d / "bin" / "nim_backup".exe, d / "bin" / "nim".exe)
proc icTest(args: string) =
temp("")
let inp = os.parseCmdLine(args)[0]
proc runIcTestFile(inp: string) =
## Compile a single `tests/ic` file with `nim ic`, once per `#!EDIT!#` fragment
## (each fragment is the file's source after that incremental edit). Only checks
## that `nim ic` exits 0 — the produced binary's output is not verified here.
let content = readFile(inp)
let nimExe = getAppDir() / "bin" / "nim_temp".exe
var i = 0
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 "
cmd.add quoteShell(file)
exec(cmd)
inc i
# The `tests/ic` files that `nim ic` must keep compiling. Multi-module tests rely
# on a sibling helper (`timp` -> `myimp`, `tcompiletimeglobal` -> `mctglobal`),
# which exercises the NIF import/load path the single-file tests do not.
const icSuite = ["thallo", "tconverter", "timp", "tmiscs", "tparseutils",
"tcompiletimeglobal", "tsighashstable", "tpureenum", "tgenericoffer",
"tconverterreexport"]
proc icTest(args: string) =
temp("")
let parsed = os.parseCmdLine(args)
if parsed.len > 0 and parsed[0].len > 0:
# `koch ic <file>`: run just that file.
runIcTestFile(parsed[0])
else:
# `koch ic`: the full regression set we want to keep working — the test
# suite plus both self-host bootstraps (`bootic` and `bootic -d:release`).
for t in icSuite:
runIcTestFile("tests" / "ic" / (t & ".nim"))
bootic("", skipIntegrityCheck = false)
bootic("-d:release", skipIntegrityCheck = false)
proc buildDrNim(args: string) =
if not dirExists("dist/nimz3"):
@@ -744,6 +814,7 @@ when isMainModule:
of cmdArgument:
case normalize(op.key)
of "boot": boot(op.cmdLineRest, skipIntegrityCheck)
of "bootic": bootic(op.cmdLineRest, skipIntegrityCheck)
of "clean": clean(op.cmdLineRest)
of "doc", "docs": buildDocs(op.cmdLineRest & " --d:nimPreviewSlimSystem " & paCode, localDocsOnly, localDocsOut)
of "doc0", "docs0":

View File

@@ -7,21 +7,21 @@
#
when defined(js):
{.error: "This library needs to be compiled with a c-like backend, and depends on PCRE2; See jsre for JS backend.".}
{.error: "This library needs to be compiled with a c-like backend, and depends on PCRE; See jsre for JS backend.".}
## .. warning:: NRE is deprecated.
## Use [Regex](https://github.com/nitely/nim-regex) or
## `NRE2 <nre2.html>`_ that wraps Regex so that you can easily replace NRE.
## This compatibility module uses PCRE2.
## PCRE library is now at end of life.
##
## What is NRE?
## ============
##
## A regular expression library for Nim using PCRE2 to do the hard work.
## A regular expression library for Nim using PCRE to do the hard work.
##
## For documentation on how to write patterns, there exists `the official PCRE2
## For documentation on how to write patterns, there exists `the official PCRE
## pattern documentation
## <https://www.pcre.org/current/doc/html/pcre2pattern.html>`_. You can also
## <https://www.pcre.org/original/doc/html/pcrepattern.html>`_. You can also
## search the internet for a wide variety of third-party documentation and
## tools.
##
@@ -39,8 +39,10 @@ runnableExamples:
## Licencing
## ---------
##
## PCRE2 is distributed under a BSD-style licence.
## PCRE has `some additional terms`_ that you must agree to in order to use
## this module.
##
## .. _`some additional terms`: https://pcre.sourceforge.net/license.txt
runnableExamples:
import std/sugar
let vowels = re"[aeoui]"
@@ -64,7 +66,7 @@ runnableExamples:
assert find("uxabc", re"(?<=x|y)ab", start = 1).get.captures[-1] == "ab"
assert find("uxabc", re"ab", start = 3).isNone
import std/pcre2 as pcre
from std/pcre import nil
import nre/private/util
import std/tables
from std/strutils import `%`
@@ -80,6 +82,7 @@ type
RegexDesc* = object
pattern*: string
pcreObj: ptr pcre.Pcre ## not nil
pcreExtra: ptr pcre.ExtraData ## nil
captureNameToId: Table[string, int]
@@ -90,9 +93,9 @@ type
##
## `pattern: string`
## : the string that was used to create the pattern. For details on how
## to write a pattern, please see `the official PCRE2 pattern
## to write a pattern, please see `the official PCRE pattern
## documentation.
## <https://www.pcre.org/current/doc/html/pcre2pattern.html>`_
## <https://www.pcre.org/original/doc/html/pcrepattern.html>`_
##
## `captureCount: int`
## : the number of captures that the pattern has.
@@ -137,23 +140,23 @@ type
## NEL (next line, U+0085), LS (line separator, U+2028), and PS
## (paragraph separator, U+2029). For the 8-bit library, the last two
## are recognized only in UTF-8 mode.
## -- man pcre2pattern
## man pcre
##
## - `(*JAVASCRIPT_COMPAT)` - JavaScript compatibility
## - `(*NO_STUDY)` - turn off studying; study is enabled by default
##
## For more details on the leading option groups, see the `Option
## Setting <https://www.pcre.org/current/doc/html/pcre2syntax.html#SEC16>`_
## Setting <https://man7.org/linux/man-pages/man3/pcresyntax.3.html#OPTION_SETTING>`_
## and the `Newline
## Convention <https://www.pcre.org/current/doc/html/pcre2syntax.html#SEC17>`_
## sections of the `PCRE2 syntax
## manual <https://www.pcre.org/current/doc/html/pcre2syntax.html>`_.
## Convention <https://man7.org/linux/man-pages/man3/pcresyntax.3.html#NEWLINE_CONVENTION>`_
## sections of the `PCRE syntax
## manual <https://man7.org/linux/man-pages/man3/pcresyntax.3.html>`_.
##
## Some of these options are not part of a pattern and are converted by nre
## into PCRE2 flags. These include `NEVER_UTF`, `ANCHORED`,
## Some of these options are not part of PCRE and are converted by nre
## into PCRE flags. These include `NEVER_UTF`, `ANCHORED`,
## `DOLLAR_ENDONLY`, `FIRSTLINE`, `NO_AUTO_CAPTURE`,
## `JAVASCRIPT_COMPAT`, `U`, `NO_STUDY`. In other PCRE2 wrappers, you
## will need to pass these as separate flags to PCRE2.
## `JAVASCRIPT_COMPAT`, `U`, `NO_STUDY`. In other PCRE wrappers, you
## will need to pass these as separate flags to PCRE.
RegexMatch* = object
## Usually seen as `Option[RegexMatch]`, it represents the result of an
@@ -193,7 +196,7 @@ type
pattern*: Regex ## The regex doing the matching.
## Not nil.
str*: string ## The string that was matched against.
pcreMatchBounds: seq[HSlice[csize_t, csize_t]] ## First item is the bounds of the match
pcreMatchBounds: seq[HSlice[cint, cint]] ## First item is the bounds of the match
## Other items are the captures
## `a` is inclusive start, `b` is exclusive end
@@ -224,32 +227,38 @@ when defined(gcDestructors):
when defined(nimAllowNonVarDestructor) and defined(nimPreviewNonVarDestructor):
proc `=destroy`(pattern: RegexDesc) =
`=destroy`(pattern.pattern)
pcre.code_free(pattern.pcreObj)
pcre.free_substring(cast[cstring](pattern.pcreObj))
if pattern.pcreExtra != nil:
pcre.free_study(pattern.pcreExtra)
`=destroy`(pattern.captureNameToId)
else:
proc `=destroy`(pattern: var RegexDesc) =
`=destroy`(pattern.pattern)
pcre.code_free(pattern.pcreObj)
pcre.free_substring(cast[cstring](pattern.pcreObj))
if pattern.pcreExtra != nil:
pcre.free_study(pattern.pcreExtra)
`=destroy`(pattern.captureNameToId)
else:
proc destroyRegex(pattern: Regex) =
`=destroy`(pattern.pattern)
pcre.code_free(pattern.pcreObj)
pcre.free_substring(cast[cstring](pattern.pcreObj))
if pattern.pcreExtra != nil:
pcre.free_study(pattern.pcreExtra)
`=destroy`(pattern.captureNameToId)
proc getinfo[T](pattern: Regex, opt: uint32): T =
proc getinfo[T](pattern: Regex, opt: cint): T =
result = default(T)
let retcode = pcre.pattern_info(pattern.pcreObj, opt, addr result)
let retcode = pcre.fullinfo(pattern.pcreObj, pattern.pcreExtra, opt, addr result)
if retcode < 0:
# XXX Error message that doesn't expose implementation details
raise newException(FieldDefect, "Invalid getinfo for $1, errno $2" % [$opt, $retcode])
proc getNameToNumberTable(pattern: Regex): Table[string, int] =
let entryCount = getinfo[uint32](pattern, pcre.INFO_NAMECOUNT).int
let entrySize = getinfo[uint32](pattern, pcre.INFO_NAMEENTRYSIZE).int
let entryCount = getinfo[cint](pattern, pcre.INFO_NAMECOUNT)
let entrySize = getinfo[cint](pattern, pcre.INFO_NAMEENTRYSIZE)
let table = cast[ptr UncheckedArray[uint8]](
getinfo[pointer](pattern, pcre.INFO_NAMETABLE))
getinfo[int](pattern, pcre.INFO_NAMETABLE))
result = initTable[string, int]()
@@ -265,69 +274,61 @@ proc getNameToNumberTable(pattern: Regex): Table[string, int] =
result[name] = num
proc pcreErrorMessage(errorCode: cint): string =
var buffer: array[256, uint8]
let length = pcre.get_error_message(errorCode, addr buffer[0], buffer.len.csize_t)
if length >= 0:
result = newString(length)
if length > 0:
copyMem(addr result[0], addr buffer[0], length)
else:
result = $errorCode
proc jitCompile(pattern: ptr pcre.Pcre) =
var hasJit: cint = 0
if pcre.config(pcre.CONFIG_JIT, addr hasJit) == 0 and hasJit == 1:
discard pcre.jit_compile(pattern, pcre.JIT_COMPLETE.uint32)
proc initRegex(pattern: string, flags: uint32, study = true): Regex =
proc initRegex(pattern: string, flags: int, study = true): Regex =
when defined(gcDestructors):
result = Regex()
else:
new(result, destroyRegex)
result.pattern = pattern
var
errorCode: cint = 0
errOffset: csize_t = 0
var errorMsg: cstring = ""
var errOffset: cint = 0
result.pcreObj = pcre.compile(cast[ptr uint8](cstring(pattern)),
pattern.len.csize_t, flags, addr errorCode,
result.pcreObj = pcre.compile(cstring(pattern),
# better hope int is at least 4 bytes..
cint(flags), addr errorMsg,
addr errOffset, nil)
if result.pcreObj == nil:
# failed to compile
raise SyntaxError(msg: pcreErrorMessage(errorCode), pos: errOffset.int,
pattern: pattern)
raise SyntaxError(msg: $errorMsg, pos: errOffset, pattern: pattern)
if study:
jitCompile(result.pcreObj)
var options: cint = 0
var hasJit: cint = cint(0)
if pcre.config(pcre.CONFIG_JIT, addr hasJit) == 0:
if hasJit == 1'i32:
options = pcre.STUDY_JIT_COMPILE
result.pcreExtra = pcre.study(result.pcreObj, options, addr errorMsg)
if errorMsg != nil:
raise StudyError(msg: $errorMsg)
result.captureNameToId = result.getNameToNumberTable()
proc captureCount*(pattern: Regex): int =
return getinfo[uint32](pattern, pcre.INFO_CAPTURECOUNT).int
return getinfo[cint](pattern, pcre.INFO_CAPTURECOUNT)
proc captureNameId*(pattern: Regex): Table[string, int] =
return pattern.captureNameToId
proc matchesCrLf(pattern: Regex): bool =
let newline = getinfo[uint32](pattern, pcre.INFO_NEWLINE)
case newline
of pcre.NEWLINE_CRLF, pcre.NEWLINE_ANY, pcre.NEWLINE_ANYCRLF:
let flags = uint32(getinfo[culong](pattern, pcre.INFO_OPTIONS))
let newlineFlags = flags and (pcre.NEWLINE_CRLF or
pcre.NEWLINE_ANY or
pcre.NEWLINE_ANYCRLF)
if newlineFlags > 0u32:
return true
of pcre.NEWLINE_CR, pcre.NEWLINE_LF, pcre.NEWLINE_NUL:
return false
else:
discard
# get flags from build config
var confFlags: uint32 = 0
var confFlags: cint = cint(0)
if pcre.config(pcre.CONFIG_NEWLINE, addr confFlags) != 0:
assert(false, "CONFIG_NEWLINE apparently got screwed up")
case confFlags
of pcre.NEWLINE_CR, pcre.NEWLINE_LF, pcre.NEWLINE_NUL: return false
of pcre.NEWLINE_CRLF, pcre.NEWLINE_ANY, pcre.NEWLINE_ANYCRLF: return true
of 13: return false
of 10: return false
of (13 shl 8) or 10: return true
of -2: return true
of -1: return true
else: return false
@@ -337,9 +338,7 @@ func captures*(pattern: RegexMatch): Captures = return Captures(pattern)
func contains*(pattern: CaptureBounds, i: int): bool =
let pattern = RegexMatch(pattern)
let index = i + 1
index >= 0 and index < pattern.pcreMatchBounds.len and
pattern.pcreMatchBounds[index].a != pcre.UNSET
pattern.pcreMatchBounds[i + 1].a != -1
func contains*(pattern: Captures, i: int): bool =
i in CaptureBounds(pattern)
@@ -350,7 +349,7 @@ func `[]`*(pattern: CaptureBounds, i: int): HSlice[int, int] =
raise newException(IndexDefect, "Group '" & $i & "' was not captured")
let bounds = pattern.pcreMatchBounds[i + 1]
int(bounds.a) .. (int(bounds.b) - 1)
int(bounds.a)..int(bounds.b-1)
func `[]`*(pattern: Captures, i: int): string =
let pattern = RegexMatch(pattern)
@@ -438,7 +437,8 @@ proc `$`*(pattern: RegexMatch): string =
proc `==`*(a, b: Regex): bool =
if not a.isNil and not b.isNil:
return a.pattern == b.pattern and
a.pcreObj == b.pcreObj
a.pcreObj == b.pcreObj and
a.pcreExtra == b.pcreExtra
else:
return system.`==`(a, b)
@@ -453,7 +453,7 @@ const PcreOptions = {
"FIRSTLINE": pcre.FIRSTLINE,
"NO_AUTO_CAPTURE": pcre.NO_AUTO_CAPTURE,
"JAVASCRIPT_COMPAT": pcre.JAVASCRIPT_COMPAT,
"U": pcre.UTF or pcre.UCP
"U": pcre.UTF8 or pcre.UCP
}.toTable
# Options that are supported inside regular expressions themselves
@@ -503,63 +503,46 @@ proc extractOptions(pattern: string): tuple[pattern: string, flags: int, study:
proc re*(pattern: string): Regex =
let (pattern, flags, study) = extractOptions(pattern)
initRegex(pattern, cast[uint32](flags), study)
initRegex(pattern, flags, study)
func isInvalidUnicodeError(errorCode: cint): bool =
(errorCode <= pcre.ERROR_UTF8_ERR1 and errorCode >= pcre.ERROR_UTF8_ERR21) or
errorCode == pcre.ERROR_BADUTFOFFSET or
errorCode == pcre.ERROR_DFA_UINVALID_UTF
proc newMatchData(pattern: Regex): ptr pcre.MatchData =
result = pcre.match_data_create_from_pattern(pattern.pcreObj, nil)
if result == nil:
raise RegexInternalError(msg: "could not allocate PCRE2 match data")
proc matchImpl(str: string, pattern: Regex, start, endpos: int, options: uint32): Option[RegexMatch] =
proc matchImpl(str: string, pattern: Regex, start, endpos: int, flags: int): Option[RegexMatch] =
var myResult = RegexMatch(pattern: pattern, str: str)
myResult.pcreMatchBounds = newSeq[HSlice[csize_t, csize_t]](pattern.captureCount() + 1)
# See PCRE man pages.
# 2x capture count to make room for start-end pairs
# 1x capture count as slack space for PCRE
let vecsize = (pattern.captureCount() + 1) * 3
# div 2 because each element is 2 cints long
# plus 1 because we need the ceiling, not the floor
myResult.pcreMatchBounds = newSeq[HSlice[cint, cint]]((vecsize + 1) div 2)
myResult.pcreMatchBounds.setLen(vecsize div 3)
let strlen = if endpos == int.high: str.len else: endpos+1
doAssert(strlen <= str.len) # don't want buffer overflows
if start < 0 or start > strlen:
return none(RegexMatch)
let matchData = newMatchData(pattern)
defer: pcre.match_data_free(matchData)
let execRet = pcre.match(pattern.pcreObj,
cast[ptr uint8](cstring(str)),
strlen.csize_t,
start.csize_t,
options,
matchData,
nil)
let rawMatches = cast[ptr UncheckedArray[csize_t]](pcre.get_ovector_pointer(matchData))
let ovectorCount = min(myResult.pcreMatchBounds.len,
pcre.get_ovector_count(matchData).int)
for i in 0 ..< ovectorCount:
myResult.pcreMatchBounds[i] = rawMatches[i * 2] .. rawMatches[i * 2 + 1]
let execRet = pcre.exec(pattern.pcreObj,
pattern.pcreExtra,
cstring(str),
cint(strlen),
cint(start),
cint(flags),
cast[ptr cint](addr myResult.pcreMatchBounds[0]),
cint(vecsize))
if execRet >= 0:
return some(myResult)
if isInvalidUnicodeError(execRet):
let errorPos = if myResult.pcreMatchBounds.len > 0 and
myResult.pcreMatchBounds[0].a != pcre.UNSET:
myResult.pcreMatchBounds[0].a.int
case execRet:
of pcre.ERROR_NOMATCH:
return none(RegexMatch)
of pcre.ERROR_NULL:
raise newException(AccessViolationDefect, "Expected non-null parameters")
of pcre.ERROR_BADOPTION:
raise RegexInternalError(msg: "Unknown pattern flag. Either a bug or " &
"outdated PCRE.")
of pcre.ERROR_BADUTF8, pcre.ERROR_SHORTUTF8, pcre.ERROR_BADUTF8_OFFSET:
raise InvalidUnicodeError(msg: "Invalid unicode byte sequence",
pos: myResult.pcreMatchBounds[0].a)
else:
start
raise InvalidUnicodeError(msg: "Invalid unicode byte sequence", pos: errorPos)
case execRet
of pcre.ERROR_NOMATCH:
return none(RegexMatch)
of pcre.ERROR_NULL:
raise newException(AccessViolationDefect, "Expected non-null parameters")
of pcre.ERROR_BADOPTION:
raise RegexInternalError(msg: "Unknown pattern flag. Either a bug or " &
"outdated PCRE2.")
else:
raise RegexInternalError(msg: "Unknown internal error: " & $execRet)
raise RegexInternalError(msg: "Unknown internal error: " & $execRet)
proc match*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] =
## Like `find(...)<#find,string,Regex,int>`_, but anchored to the start of the
@@ -576,7 +559,7 @@ proc match*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[R
assert 0 in "abc".match(re"(\w)").get.captureBounds
assert "abc".match(re"").get.captureBounds[-1] == 0 .. -1
assert "abc".match(re"abc").get.captureBounds[-1] == 0 .. 2
return str.matchImpl(pattern, start, endpos, cast[uint32](pcre.ANCHORED))
return str.matchImpl(pattern, start, endpos, pcre.ANCHORED)
iterator findIter*(str: string, pattern: Regex, start = 0, endpos = int.high): RegexMatch =
## Works the same as `find(...)<#find,string,Regex,int>`_, but finds every
@@ -590,21 +573,21 @@ iterator findIter*(str: string, pattern: Regex, start = 0, endpos = int.high): R
## Variants:
##
## - `proc findAll(...)` returns a `seq[string]`
# see pcre2demo for explanation => https://www.pcre.org/current/doc/html/pcre2demo.html
# see pcredemo for explanation => https://www.pcre.org/original/doc/html/pcredemo.html
let matchesCrLf = pattern.matchesCrLf()
let unicode = uint32(getinfo[uint32](pattern, pcre.INFO_ALLOPTIONS) and
pcre.UTF.uint32) > 0u32
let unicode = uint32(getinfo[culong](pattern, pcre.INFO_OPTIONS) and
pcre.UTF8) > 0u32
let strlen = if endpos == int.high: str.len else: endpos+1
var offset = start
var match: Option[RegexMatch] = default(Option[RegexMatch])
var neverMatched = true
while true:
var flags = 0'u32
var flags = 0
if match.isSome and
match.get.matchBounds.a > match.get.matchBounds.b:
# 0-len match
flags = pcre.NOTEMPTY_ATSTART.uint32
flags = pcre.NOTEMPTY_ATSTART
match = str.matchImpl(pattern, offset, endpos, flags)
if match.isNone:
@@ -640,7 +623,7 @@ proc find*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[Re
## `endpos`
## : The maximum index for a match; `int.high` means the end of the
## string, otherwise its an inclusive upper bound.
return str.matchImpl(pattern, start, endpos, 0'u32)
return str.matchImpl(pattern, start, endpos, 0)
proc findAll*(str: string, pattern: Regex, start = 0, endpos = int.high): seq[string] =
result = @[]

View File

@@ -8,25 +8,27 @@
#
when defined(js):
{.error: "This library needs to be compiled with a c-like backend, and depends on PCRE2; See jsre for JS backend.".}
{.error: "This library needs to be compiled with a c-like backend, and depends on PCRE; See jsre for JS backend.".}
## .. warning:: This module is deprecated.
## Use [Regex](https://github.com/nitely/nim-regex).
## This compatibility module uses PCRE2.
## PCRE library is now at end of life.
##
## Regular expression support for Nim.
##
## This module is implemented by providing a wrapper around the
## `PCRE2 (Perl-Compatible Regular Expressions) <https://www.pcre.org>`_
## C library. This means that your application will depend on the PCRE2
## `PCRE (Perl-Compatible Regular Expressions) <https://www.pcre.org>`_
## C library. This means that your application will depend on the PCRE
## library's licence when using this module, which should not be a problem
## though.
##
## .. note:: There are also alternative nimble packages such as [tinyre](https://github.com/khchen/tinyre)
## and [regex](https://github.com/nitely/nim-regex).
##
## PCRE2 is distributed under a BSD-style licence.
## PCRE's licence follows:
##
## .. include:: ../../doc/regexprs.txt
##
runnableExamples:
## Unless specified otherwise, `start` parameter in each proc indicates
@@ -38,7 +40,7 @@ runnableExamples:
# can't match start of string since we're starting at 1
import
std/[pcre2, strutils]
std/[pcre, strutils, rtarrays]
when defined(nimPreviewSlimSystem):
import std/syncio
@@ -58,7 +60,8 @@ type
## expression will be used only once)
RegexDesc = object
h: ptr pcre2.Pcre
h: ptr Pcre
e: ptr ExtraData
Regex* = ref RegexDesc ## a compiled regular expression
@@ -68,10 +71,14 @@ type
when defined(gcDestructors):
when defined(nimAllowNonVarDestructor):
proc `=destroy`(x: RegexDesc) =
pcre2.code_free(x.h)
pcre.free_substring(cast[cstring](x.h))
if not isNil(x.e):
pcre.free_study(x.e)
else:
proc `=destroy`(x: var RegexDesc) =
pcre2.code_free(x.h)
pcre.free_substring(cast[cstring](x.h))
if not isNil(x.e):
pcre.free_study(x.e)
proc raiseInvalidRegex(msg: string) {.noinline, noreturn.} =
var e: ref RegexError
@@ -79,43 +86,21 @@ proc raiseInvalidRegex(msg: string) {.noinline, noreturn.} =
e.msg = msg
raise e
proc pcre2ErrorMessage(errorCode: cint): string =
var buffer: array[256, uint8]
let length = pcre2.get_error_message(errorCode, addr buffer[0], buffer.len.csize_t)
if length >= 0:
result = newString(length)
if length > 0:
copyMem(addr result[0], addr buffer[0], length)
else:
result = $errorCode
proc rawCompile(pattern: string, options: uint32): ptr pcre2.Pcre =
proc rawCompile(pattern: string, flags: cint): ptr Pcre =
var
errorCode: cint = 0
offset: csize_t = 0
result = pcre2.compile(cast[ptr uint8](pattern.cstring), pattern.len.csize_t,
options, addr errorCode, addr offset, nil)
msg: cstring = ""
offset: cint = 0
result = pcre.compile(pattern, flags, addr(msg), addr(offset), nil)
if result == nil:
raiseInvalidRegex(pcre2ErrorMessage(errorCode) & "\n" & pattern & "\n" &
spaces(offset.int) & "^\n")
raiseInvalidRegex($msg & "\n" & pattern & "\n" & spaces(offset) & "^\n")
proc finalizeRegEx(x: Regex) =
pcre2.code_free(x.h)
func toPcre2Options(flags: set[RegexFlag]): uint32 =
if reIgnoreCase in flags:
result = result or pcre2.CASELESS.uint32
if reMultiLine in flags:
result = result or pcre2.MULTILINE.uint32
if reDotAll in flags:
result = result or pcre2.DOTALL.uint32
if reExtended in flags:
result = result or pcre2.EXTENDED.uint32
proc jitCompile(pattern: ptr pcre2.Pcre) =
var hasJit: cint = 0
if pcre2.config(pcre2.CONFIG_JIT, addr hasJit) == 0 and hasJit == 1:
discard pcre2.jit_compile(pattern, pcre2.JIT_COMPLETE.uint32)
# XXX This is a hack, but PCRE does not export its "free" function properly.
# Sigh. The hack relies on PCRE's implementation (see `pcre_get.c`).
# Fortunately the implementation is unlikely to change.
pcre.free_substring(cast[cstring](x.h))
if not isNil(x.e):
pcre.free_study(x.e)
proc re*(s: string, flags = {reStudy}): Regex =
## Constructor of regular expressions.
@@ -131,9 +116,16 @@ proc re*(s: string, flags = {reStudy}): Regex =
result = Regex()
else:
new(result, finalizeRegEx)
result.h = rawCompile(s, toPcre2Options(flags))
result.h = rawCompile(s, cast[cint](flags - {reStudy}))
if reStudy in flags:
jitCompile(result.h)
var msg: cstring = ""
var options: cint = 0
var hasJit: cint = 0
if pcre.config(pcre.CONFIG_JIT, addr hasJit) == 0:
if hasJit == 1'i32:
options = pcre.STUDY_JIT_COMPILE
result.e = pcre.study(result.h, options, addr msg)
if not isNil(msg): raiseInvalidRegex($msg)
proc rex*(s: string, flags = {reStudy, reExtended}): Regex =
## Constructor for extended regular expressions.
@@ -150,58 +142,25 @@ proc bufSubstr(b: cstring, sPos, ePos: int): string {.inline.} =
copyMem(addr(result[0]), unsafeAddr(b[sPos]), sz)
result.setLen(sz)
proc newMatchData(slots: int): ptr pcre2.MatchData =
result = pcre2.match_data_create(max(slots, 1).uint32, nil)
if result == nil:
raiseInvalidRegex("could not allocate PCRE2 match data")
template ovector(matchData: ptr pcre2.MatchData): ptr UncheckedArray[csize_t] =
cast[ptr UncheckedArray[csize_t]](pcre2.get_ovector_pointer(matchData))
proc rawMatch(buf: cstring, pattern: Regex, start, bufSize: int,
options: uint32, matchData: ptr pcre2.MatchData): cint =
if start < 0 or bufSize < 0:
return pcre2.ERROR_BADOFFSET
pcre2.match(pattern.h, cast[ptr uint8](buf), bufSize.csize_t,
start.csize_t, options, matchData, nil)
proc copyStringMatches(buf: cstring, rawMatches: ptr UncheckedArray[csize_t],
captureCount: int, matches: var openArray[string]) =
let upper = min(captureCount - 1, matches.len)
if upper > 0:
for i in 1 .. upper:
let matchStart = rawMatches[i * 2]
let matchEnd = rawMatches[i * 2 + 1]
if matchStart != pcre2.UNSET:
matches[i-1] = bufSubstr(buf, int(matchStart), int(matchEnd))
else:
matches[i-1] = ""
proc copyBoundsMatches(rawMatches: ptr UncheckedArray[csize_t],
captureCount: int,
matches: var openArray[tuple[first, last: int]]) =
let upper = min(captureCount - 1, matches.len)
if upper > 0:
for i in 1 .. upper:
let matchStart = rawMatches[i * 2]
let matchEnd = rawMatches[i * 2 + 1]
if matchStart != pcre2.UNSET:
matches[i-1] = (int(matchStart), int(matchEnd) - 1)
else:
matches[i-1] = (-1, 0)
proc matchOrFind(buf: cstring, pattern: Regex, matches: var openArray[string],
start, bufSize: int, options: uint32): int =
let matchData = newMatchData(matches.len + 1)
defer: pcre2.match_data_free(matchData)
let res = rawMatch(buf, pattern, start, bufSize, options, matchData)
let rawMatches = ovector(matchData)
if res < 0: return int(res)
copyStringMatches(buf, rawMatches, int(res), matches)
return int(rawMatches[1]) - int(rawMatches[0])
start, bufSize, flags: cint): cint =
var
rtarray = initRtArray[cint]((matches.len+1)*3)
rawMatches = rtarray.getRawData
res = pcre.exec(pattern.h, pattern.e, buf, bufSize, start, flags,
cast[ptr cint](rawMatches), (matches.len+1).cint*3)
if res < 0'i32: return res
for i in 1..int(res)-1:
var a = rawMatches[i * 2]
var b = rawMatches[i * 2 + 1]
if a >= 0'i32:
matches[i-1] = bufSubstr(buf, int(a), int(b))
else: matches[i-1] = ""
return rawMatches[1] - rawMatches[0]
const MaxReBufSize* = high(int)
## Maximum PCRE2 buffer start/size accepted by this Nim API.
const MaxReBufSize* = high(cint)
## Maximum PCRE (API 1) buffer start/size equal to `high(cint)`, which even
## for 64-bit systems can be either 2`31`:sup:-1 or 2`63`:sup:-1.
proc findBounds*(buf: cstring, pattern: Regex, matches: var openArray[string],
start = 0, bufSize: int): tuple[first, last: int] =
@@ -213,12 +172,17 @@ proc findBounds*(buf: cstring, pattern: Regex, matches: var openArray[string],
##
## Note: The memory for `matches` needs to be allocated before this function is
## called, otherwise it will just remain empty.
let matchData = newMatchData(matches.len + 1)
defer: pcre2.match_data_free(matchData)
let res = rawMatch(buf, pattern, start, bufSize, 0'u32, matchData)
let rawMatches = ovector(matchData)
if res < 0: return (-1, 0)
copyStringMatches(buf, rawMatches, int(res), matches)
var
rtarray = initRtArray[cint]((matches.len+1)*3)
rawMatches = rtarray.getRawData
res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, 0'i32,
cast[ptr cint](rawMatches), (matches.len+1).cint*3)
if res < 0'i32: return (-1, 0)
for i in 1..int(res)-1:
var a = rawMatches[i * 2]
var b = rawMatches[i * 2 + 1]
if a >= 0'i32: matches[i-1] = bufSubstr(buf, int(a), int(b))
else: matches[i-1] = ""
return (rawMatches[0].int, rawMatches[1].int - 1)
proc findBounds*(s: string, pattern: Regex, matches: var openArray[string],
@@ -248,12 +212,17 @@ proc findBounds*(buf: cstring, pattern: Regex,
## `(-1,0)` is returned.
##
## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty.
let matchData = newMatchData(matches.len + 1)
defer: pcre2.match_data_free(matchData)
let res = rawMatch(buf, pattern, start, bufSize, 0'u32, matchData)
let rawMatches = ovector(matchData)
if res < 0: return (-1, 0)
copyBoundsMatches(rawMatches, int(res), matches)
var
rtarray = initRtArray[cint]((matches.len+1)*3)
rawMatches = rtarray.getRawData
res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, 0'i32,
cast[ptr cint](rawMatches), (matches.len+1).cint*3)
if res < 0'i32: return (-1, 0)
for i in 1..int(res)-1:
var a = rawMatches[i * 2]
var b = rawMatches[i * 2 + 1]
if a >= 0'i32: matches[i-1] = (int(a), int(b)-1)
else: matches[i-1] = (-1,0)
return (rawMatches[0].int, rawMatches[1].int - 1)
proc findBounds*(s: string, pattern: Regex,
@@ -275,28 +244,29 @@ proc findBounds*(s: string, pattern: Regex,
min(start, MaxReBufSize), min(s.len, MaxReBufSize))
proc findBoundsImpl(buf: cstring, pattern: Regex,
start = 0, bufSize = 0,
options = 0'u32): tuple[first, last: int] =
let matchData = newMatchData(1)
defer: pcre2.match_data_free(matchData)
let res = rawMatch(buf, pattern, start, bufSize, options, matchData)
let rawMatches = ovector(matchData)
if res < 0:
start = 0, bufSize = 0, flags = 0): tuple[first, last: int] =
var rtarray = initRtArray[cint](3)
let rawMatches = rtarray.getRawData
let res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, flags.int32,
cast[ptr cint](rawMatches), 3)
if res < 0'i32:
result = (-1, 0)
else:
result = (int(rawMatches[0]), int(rawMatches[1]) - 1)
result = (int(rawMatches[0]), int(rawMatches[1]-1))
proc findBounds*(buf: cstring, pattern: Regex,
start = 0, bufSize: int): tuple[first, last: int] =
## returns the `first` and `last` position of `pattern` in `buf`,
## where `buf` has length `bufSize` (not necessarily `'\0'` terminated).
## If it does not match, `(-1,0)` is returned.
let matchData = newMatchData(1)
defer: pcre2.match_data_free(matchData)
let res = rawMatch(buf, pattern, start, bufSize, 0'u32, matchData)
let rawMatches = ovector(matchData)
if res < 0: return (int(res), 0)
return (int(rawMatches[0]), int(rawMatches[1]) - 1)
var
rtarray = initRtArray[cint](3)
rawMatches = rtarray.getRawData
res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, 0'i32,
cast[ptr cint](rawMatches), 3)
if res < 0'i32: return (int(res), 0)
return (int(rawMatches[0]), int(rawMatches[1]-1))
proc findBounds*(s: string, pattern: Regex,
start = 0): tuple[first, last: int] {.inline.} =
@@ -309,16 +279,14 @@ proc findBounds*(s: string, pattern: Regex,
result = findBounds(cstring(s), pattern,
min(start, MaxReBufSize), min(s.len, MaxReBufSize))
proc matchOrFind(buf: cstring, pattern: Regex, start, bufSize: int,
options: uint32): int =
let matchData = newMatchData(1)
defer: pcre2.match_data_free(matchData)
let res = rawMatch(buf, pattern, start, bufSize, options, matchData)
if res >= 0:
let rawMatches = ovector(matchData)
result = int(rawMatches[1]) - int(rawMatches[0])
else:
result = int(res)
proc matchOrFind(buf: cstring, pattern: Regex, start, bufSize: int, flags: cint): cint =
var
rtarray = initRtArray[cint](3)
rawMatches = rtarray.getRawData
result = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, flags,
cast[ptr cint](rawMatches), 3)
if result >= 0'i32:
result = rawMatches[1] - rawMatches[0]
proc matchLen*(s: string, pattern: Regex, matches: var openArray[string],
start = 0): int {.inline.} =
@@ -327,7 +295,7 @@ proc matchLen*(s: string, pattern: Regex, matches: var openArray[string],
## of zero can happen.
##
## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty.
result = matchOrFind(cstring(s), pattern, matches, start, s.len, cast[uint32](pcre2.ANCHORED))
result = matchOrFind(cstring(s), pattern, matches, start.cint, s.len.cint, pcre.ANCHORED)
proc matchLen*(buf: cstring, pattern: Regex, matches: var openArray[string],
start = 0, bufSize: int): int {.inline.} =
@@ -336,7 +304,7 @@ proc matchLen*(buf: cstring, pattern: Regex, matches: var openArray[string],
## of zero can happen.
##
## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty.
return matchOrFind(buf, pattern, matches, start, bufSize, cast[uint32](pcre2.ANCHORED))
return matchOrFind(buf, pattern, matches, start.cint, bufSize.cint, pcre.ANCHORED)
proc matchLen*(s: string, pattern: Regex, start = 0): int {.inline.} =
## the same as `match`, but it returns the length of the match,
@@ -347,13 +315,13 @@ proc matchLen*(s: string, pattern: Regex, start = 0): int {.inline.} =
doAssert matchLen("abcdefg", re"cde", 2) == 3
doAssert matchLen("abcdefg", re"abcde") == 5
doAssert matchLen("abcdefg", re"cde") == -1
result = matchOrFind(cstring(s), pattern, start, s.len, cast[uint32](pcre2.ANCHORED))
result = matchOrFind(cstring(s), pattern, start.cint, s.len.cint, pcre.ANCHORED)
proc matchLen*(buf: cstring, pattern: Regex, start = 0, bufSize: int): int {.inline.} =
## the same as `match`, but it returns the length of the match,
## if there is no match, `-1` is returned. Note that a match length
## of zero can happen.
result = matchOrFind(buf, pattern, start, bufSize, cast[uint32](pcre2.ANCHORED))
result = matchOrFind(buf, pattern, start.cint, bufSize, pcre.ANCHORED)
proc match*(s: string, pattern: Regex, start = 0): bool {.inline.} =
## returns `true` if `s[start..]` matches the `pattern`.
@@ -393,13 +361,18 @@ proc find*(buf: cstring, pattern: Regex, matches: var openArray[string],
## `buf` has length `bufSize` (not necessarily `'\0'` terminated).
##
## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty.
let matchData = newMatchData(matches.len + 1)
defer: pcre2.match_data_free(matchData)
let res = rawMatch(buf, pattern, start, bufSize, 0'u32, matchData)
let rawMatches = ovector(matchData)
if res < 0: return int(res)
copyStringMatches(buf, rawMatches, int(res), matches)
return int(rawMatches[0])
var
rtarray = initRtArray[cint]((matches.len+1)*3)
rawMatches = rtarray.getRawData
res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, 0'i32,
cast[ptr cint](rawMatches), (matches.len+1).cint*3)
if res < 0'i32: return res
for i in 1..int(res)-1:
var a = rawMatches[i * 2]
var b = rawMatches[i * 2 + 1]
if a >= 0'i32: matches[i-1] = bufSubstr(buf, int(a), int(b))
else: matches[i-1] = ""
return rawMatches[0]
proc find*(s: string, pattern: Regex, matches: var openArray[string],
start = 0): int {.inline.} =
@@ -414,12 +387,13 @@ proc find*(buf: cstring, pattern: Regex, start = 0, bufSize: int): int =
## returns the starting position of `pattern` in `buf`,
## where `buf` has length `bufSize` (not necessarily `'\0'` terminated).
## If it does not match, `-1` is returned.
let matchData = newMatchData(1)
defer: pcre2.match_data_free(matchData)
let res = rawMatch(buf, pattern, start, bufSize, 0'u32, matchData)
let rawMatches = ovector(matchData)
if res < 0: return int(res)
return int(rawMatches[0])
var
rtarray = initRtArray[cint](3)
rawMatches = rtarray.getRawData
res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, 0'i32,
cast[ptr cint](rawMatches), 3)
if res < 0'i32: return res
return rawMatches[0]
proc find*(s: string, pattern: Regex, start = 0): int {.inline.} =
## returns the starting position of `pattern` in `s`. If it does not
@@ -439,38 +413,40 @@ iterator findAll*(s: string, pattern: Regex, start = 0): string =
##
## Note that since this is an iterator you should not modify the string you
## are iterating over: bad things could happen.
var i = start
let matchData = newMatchData(1)
defer: pcre2.match_data_free(matchData)
var
i = int32(start)
rtarray = initRtArray[cint](3)
rawMatches = rtarray.getRawData
while true:
let res = rawMatch(s.cstring, pattern, i, len(s), 0'u32, matchData)
if res < 0: break
let rawMatches = ovector(matchData)
let matchStart = rawMatches[0]
let matchEnd = rawMatches[1]
if matchStart == matchEnd and matchStart.int == i: break
yield substr(s, int(matchStart), int(matchEnd) - 1)
i = matchEnd.int
let res = pcre.exec(pattern.h, pattern.e, s, len(s).cint, i, 0'i32,
cast[ptr cint](rawMatches), 3)
if res < 0'i32: break
let a = rawMatches[0]
let b = rawMatches[1]
if a == b and a == i: break
yield substr(s, int(a), int(b)-1)
i = b
iterator findAll*(buf: cstring, pattern: Regex, start = 0, bufSize: int): string =
## Yields all matching `substrings` of `s` that match `pattern`.
##
## Note that since this is an iterator you should not modify the string you
## are iterating over: bad things could happen.
var i = start
let matchData = newMatchData(1)
defer: pcre2.match_data_free(matchData)
var
i = int32(start)
rtarray = initRtArray[cint](3)
rawMatches = rtarray.getRawData
while true:
let res = rawMatch(buf, pattern, i, bufSize, 0'u32, matchData)
if res < 0: break
let rawMatches = ovector(matchData)
let matchStart = rawMatches[0]
let matchEnd = rawMatches[1]
if matchStart == matchEnd and matchStart.int == i: break
var str = newString(int(matchEnd - matchStart))
copyMem(str[0].addr, unsafeAddr(buf[int(matchStart)]), int(matchEnd - matchStart))
let res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, i, 0'i32,
cast[ptr cint](rawMatches), 3)
if res < 0'i32: break
let a = rawMatches[0]
let b = rawMatches[1]
if a == b and a == i: break
var str = newString(b-a)
copyMem(str[0].addr, unsafeAddr(buf[a]), b-a)
yield str
i = matchEnd.int
i = b
proc findAll*(s: string, pattern: Regex, start = 0): seq[string] {.inline.} =
## returns all matching `substrings` of `s` that match `pattern`.
@@ -527,7 +503,7 @@ proc replace*(s: string, sub: Regex, by = ""): string =
doAssert "var1=key; var2=key2".replace(re"(\w+)=(\w+)", "?") == "?; ?"
result = ""
var prev = 0
var flags = 0'u32
var flags = int32(0)
while prev < s.len:
var match = findBoundsImpl(s.cstring, sub, prev, s.len, flags)
flags = 0
@@ -536,7 +512,7 @@ proc replace*(s: string, sub: Regex, by = ""): string =
add(result, by)
if match.first > match.last:
# 0-len match
flags = pcre2.NOTEMPTY_ATSTART.uint32
flags = pcre.NOTEMPTY_ATSTART
prev = match.last + 1
add(result, substr(s, prev))

View File

@@ -246,7 +246,7 @@ proc toHashSet*[A](keys: openArray[A]): HashSet[A] =
result = initHashSet[A](keys.len)
for key in items(keys): result.incl(key)
iterator items*[A](s: HashSet[A]): A =
iterator items*[A](s: HashSet[A]): lent A =
## Iterates over elements of the set `s`.
##
## If you need a sequence with the elements you can use `sequtils.toSeq
@@ -891,7 +891,7 @@ proc `$`*[A](s: OrderedSet[A]): string =
## ```
dollarImpl()
iterator items*[A](s: OrderedSet[A]): A =
iterator items*[A](s: OrderedSet[A]): lent A =
## Iterates over keys in the ordered set `s` in insertion order.
##
## If you need a sequence with the elements you can use `sequtils.toSeq

View File

@@ -15,12 +15,16 @@
## It also provides some fast iterators over lines in text files (or
## other "line-like", variable length, delimited records).
const
nimUseFallBack = defined(nintendoswitch) or defined(nimMemfileFallback)
when defined(windows):
import std/winlean
when defined(nimPreviewSlimSystem):
import std/widestrs
elif defined(posix):
import std/posix
when not nimUseFallBack:
import std/posix
else:
{.error: "the memfiles module is not supported on your operating system!".}
@@ -29,45 +33,48 @@ import std/oserrors
when defined(nimPreviewSlimSystem):
import std/[syncio, assertions]
elif nimUseFallBack:
import std/syncio
from system/ansi_c import c_memchr
proc newEIO(msg: string): ref IOError =
result = (ref IOError)(msg: msg)
proc setFileSize(fh: FileHandle, newFileSize = -1, oldSize = -1): OSErrorCode =
## Set the size of open file pointed to by `fh` to `newFileSize` if != -1,
## allocating | freeing space from the file system. This routine returns the
## last OSErrorCode found rather than raising to support old rollback/clean-up
## code style. [ Should maybe move to std/osfiles. ]
result = OSErrorCode(0)
if newFileSize < 0 or newFileSize == oldSize:
return result
when defined(windows):
var sizeHigh = int32(newFileSize shr 32)
let sizeLow = int32(newFileSize and 0xffffffff)
let status = setFilePointer(Handle fh, sizeLow, addr(sizeHigh), FILE_BEGIN)
let lastErr = osLastError()
if (status == INVALID_SET_FILE_POINTER and lastErr.int32 != NO_ERROR) or
setEndOfFile(Handle fh) == 0:
result = lastErr
else:
if newFileSize > oldSize: # grow the file
var e: cint = cint(0) # posix_fallocate truncates up when needed.
when declared(posix_fallocate):
while (e = posix_fallocate(fh, 0, newFileSize); e == EINTR):
discard
if e == EINVAL or e == EOPNOTSUPP or e == ENOSYS:
# fallback arguable; Most portable BUT allows SEGV
if ftruncate(fh, newFileSize) == -1:
when not nimUseFallBack:
proc setFileSize(fh: FileHandle, newFileSize = -1, oldSize = -1): OSErrorCode =
## Set the size of open file pointed to by `fh` to `newFileSize` if != -1,
## allocating | freeing space from the file system. This routine returns the
## last OSErrorCode found rather than raising to support old rollback/clean-up
## code style. [ Should maybe move to std/osfiles. ]
result = OSErrorCode(0)
if newFileSize < 0 or newFileSize == oldSize:
return result
when defined(windows):
var sizeHigh = int32(newFileSize shr 32)
let sizeLow = int32(newFileSize and 0xffffffff)
let status = setFilePointer(Handle fh, sizeLow, addr(sizeHigh), FILE_BEGIN)
let lastErr = osLastError()
if (status == INVALID_SET_FILE_POINTER and lastErr.int32 != NO_ERROR) or
setEndOfFile(Handle fh) == 0:
result = lastErr
else:
if newFileSize > oldSize: # grow the file
var e: cint = cint(0) # posix_fallocate truncates up when needed.
when declared(posix_fallocate):
while (e = posix_fallocate(fh, 0, newFileSize); e == EINTR):
discard
if e == EINVAL or e == EOPNOTSUPP or e == ENOSYS:
# fallback arguable; Most portable BUT allows SEGV
if ftruncate(fh, newFileSize) == -1:
result = osLastError()
else:
discard
elif e != 0:
result = osLastError()
else: # shrink the file
if ftruncate(fh.cint, newFileSize) == -1:
result = osLastError()
else:
discard
elif e != 0:
result = osLastError()
else: # shrink the file
if ftruncate(fh.cint, newFileSize) == -1:
result = osLastError()
type
MemFile* = object ## represents a memory mapped file
@@ -84,6 +91,89 @@ type
else:
handle*: cint ## **Caution**: Posix specific public field.
flags: cint ## **Caution**: Platform specific private field.
when nimUseFallBack:
backing: string
path: string
readonly: bool
allowRemap: bool
when nimUseFallBack:
proc fallbackMappedSize(backingLen, mappedSize, offset: int): int =
if mappedSize < -1:
raise newEIO("mappedSize cannot be less than -1")
if offset < 0 or offset > backingLen:
raise newEIO("offset out of bounds")
if mappedSize == -1:
result = backingLen - offset
else:
result = min(mappedSize, backingLen - offset)
proc setFallbackView(m: var MemFile, mappedSize, offset: int) =
m.size = fallbackMappedSize(m.backing.len, mappedSize, offset)
if m.size > 0:
m.mem = cast[pointer](addr m.backing[offset])
else:
m.mem = nil
proc openFallbackMemFile(filename: string, mode: FileMode, mappedSize,
offset, newFileSize: int,
allowRemap: bool): MemFile =
result = MemFile(
handle: -1,
flags: 0,
path: filename,
readonly: mode == fmRead,
allowRemap: allowRemap
)
if newFileSize != -1:
result.backing = newString(newFileSize)
else:
result.backing = readFile(filename)
setFallbackView(result, mappedSize, offset)
proc mapMemFallback(m: var MemFile, mode: FileMode,
mappedSize, offset: int): pointer =
if not m.allowRemap:
raise newException(IOError,
"Cannot remap MemFile opened with allowRemap=false")
if mode != fmRead and m.readonly:
raise newEIO("cannot write to read-only mapping")
let size = fallbackMappedSize(m.backing.len, mappedSize, offset)
if size > 0:
result = cast[pointer](addr m.backing[offset])
else:
result = nil
proc flushFallback(m: var MemFile) =
if m.readonly or m.path.len == 0:
return
writeFile(m.path, m.backing)
proc resizeFallback(m: var MemFile, newFileSize: int) =
if m.readonly:
raise newException(IOError, "Cannot resize read-only MemFile")
if not m.allowRemap:
raise newException(IOError,
"Cannot resize MemFile opened with allowRemap=false")
if m.size != m.backing.len:
raise newException(IOError, "Cannot resize partial MemFile")
let oldLen = m.backing.len
m.backing.setLen(newFileSize)
for i in oldLen ..< newFileSize:
m.backing[i] = '\0'
setFallbackView(m, newFileSize, 0)
proc closeFallback(m: var MemFile) =
if not m.readonly:
flushFallback(m)
m.mem = nil
m.size = 0
m.handle = -1
m.flags = 0
m.backing = ""
m.path = ""
m.readonly = false
m.allowRemap = false
proc mapMem*(m: var MemFile, mode: FileMode = fmRead,
mappedSize = -1, offset = 0, mapFlags = cint(-1)): pointer =
@@ -94,7 +184,7 @@ proc mapMem*(m: var MemFile, mode: FileMode = fmRead,
if mode == fmAppend:
raise newEIO("The append mode is not supported.")
var readonly = mode == fmRead
let readonly = mode == fmRead
when defined(windows):
result = mapViewOfFileEx(
m.mapHandle,
@@ -105,6 +195,8 @@ proc mapMem*(m: var MemFile, mode: FileMode = fmRead,
nil)
if result == nil:
raiseOSError(osLastError())
elif nimUseFallBack:
result = mapMemFallback(m, mode, mappedSize, offset)
else:
assert mappedSize > 0
@@ -132,6 +224,8 @@ proc unmapMem*(f: var MemFile, p: pointer, size: int) =
## via `mapMem`.
when defined(windows):
if unmapViewOfFile(p) == 0: raiseOSError(osLastError())
elif nimUseFallBack:
discard
else:
if munmap(p, size) != 0: raiseOSError(osLastError())
@@ -178,7 +272,7 @@ proc open*(filename: string, mode: FileMode = fmRead,
raise newEIO("The append mode is not supported.")
assert newFileSize == -1 or mode != fmRead
var readonly = mode == fmRead
let readonly = mode == fmRead
template rollback =
result.mem = nil
@@ -252,7 +346,10 @@ proc open*(filename: string, mode: FileMode = fmRead,
if closeHandle(result.fHandle) != 0:
result.fHandle = INVALID_HANDLE_VALUE
else:
elif nimUseFallBack:
result = openFallbackMemFile(filename, mode, mappedSize, offset,
newFileSize, allowRemap)
elif defined(posix):
template fail(errCode: OSErrorCode, msg: string) =
rollback()
if result.handle != -1: discard close(result.handle)
@@ -309,6 +406,8 @@ proc flush*(f: var MemFile; attempts: Natural = 3) =
lastErr = osLastError()
if lastErr != ERROR_LOCK_VIOLATION.OSErrorCode:
raiseOSError(lastErr)
elif nimUseFallBack:
flushFallback(f)
else:
for i in 1..attempts:
res = msync(f.mem, f.size, MS_SYNC or MS_INVALIDATE) == 0
@@ -318,59 +417,71 @@ proc flush*(f: var MemFile; attempts: Natural = 3) =
if lastErr != EBUSY.OSErrorCode:
raiseOSError(lastErr, "error flushing mapping")
proc resize*(f: var MemFile, newFileSize: int) {.raises: [IOError, OSError].} =
## Resize & re-map the file underlying an `allowRemap MemFile`. If the OS/FS
## supports it, file space is reserved to ensure room for new virtual pages.
## Caller should wait often enough for `flush` to finish to limit use of
## system RAM for write buffering, perhaps just prior to this call.
## **Note**: this assumes the entire file is mapped read-write at offset 0.
## Also, the value of `.mem` will probably change.
if newFileSize < 1: # Q: include system/bitmasks & use PageSize ?
raise newException(IOError, "Cannot resize MemFile to < 1 byte")
when defined(windows):
if not f.wasOpened:
raise newException(IOError, "Cannot resize unopened MemFile")
if f.fHandle == INVALID_HANDLE_VALUE:
raise newException(IOError,
"Cannot resize MemFile opened with allowRemap=false")
if unmapViewOfFile(f.mem) == 0 or closeHandle(f.mapHandle) == 0: # Un-do map
raiseOSError(osLastError())
if newFileSize != f.size: # Seek to size & `setEndOfFile` => allocated.
if (let e = setFileSize(f.fHandle.FileHandle, newFileSize);
e != 0.OSErrorCode): raiseOSError(e)
f.mapHandle = createFileMappingW(f.fHandle, nil, PAGE_READWRITE, 0,0,nil)
if f.mapHandle == 0: # Re-do map
raiseOSError(osLastError())
let m = mapViewOfFileEx(f.mapHandle, FILE_MAP_READ or FILE_MAP_WRITE,
0, 0, WinSizeT(newFileSize), nil)
if m != nil:
f.mem = m
when nimUseFallBack:
proc resize*(f: var MemFile, newFileSize: int) {.raises: [IOError].} =
## Resize & re-map the file underlying an `allowRemap MemFile`. If the OS/FS
## supports it, file space is reserved to ensure room for new virtual pages.
## Caller should wait often enough for `flush` to finish to limit use of
## system RAM for write buffering, perhaps just prior to this call.
## **Note**: this assumes the entire file is mapped read-write at offset 0.
## Also, the value of `.mem` will probably change.
if newFileSize < 1: # Q: include system/bitmasks & use PageSize ?
raise newException(IOError, "Cannot resize MemFile to < 1 byte")
resizeFallback(f, newFileSize)
else:
proc resize*(f: var MemFile, newFileSize: int) {.raises: [IOError, OSError].} =
## Resize & re-map the file underlying an `allowRemap MemFile`. If the OS/FS
## supports it, file space is reserved to ensure room for new virtual pages.
## Caller should wait often enough for `flush` to finish to limit use of
## system RAM for write buffering, perhaps just prior to this call.
## **Note**: this assumes the entire file is mapped read-write at offset 0.
## Also, the value of `.mem` will probably change.
if newFileSize < 1: # Q: include system/bitmasks & use PageSize ?
raise newException(IOError, "Cannot resize MemFile to < 1 byte")
when defined(windows):
if not f.wasOpened:
raise newException(IOError, "Cannot resize unopened MemFile")
if f.fHandle == INVALID_HANDLE_VALUE:
raise newException(IOError,
"Cannot resize MemFile opened with allowRemap=false")
if unmapViewOfFile(f.mem) == 0 or closeHandle(f.mapHandle) == 0: # Un-do map
raiseOSError(osLastError())
if newFileSize != f.size: # Seek to size & `setEndOfFile` => allocated.
if (let e = setFileSize(f.fHandle.FileHandle, newFileSize);
e != 0.OSErrorCode): raiseOSError(e)
f.mapHandle = createFileMappingW(f.fHandle, nil, PAGE_READWRITE, 0,0,nil)
if f.mapHandle == 0: # Re-do map
raiseOSError(osLastError())
let m = mapViewOfFileEx(f.mapHandle, FILE_MAP_READ or FILE_MAP_WRITE,
0, 0, WinSizeT(newFileSize), nil)
if m != nil:
f.mem = m
f.size = newFileSize
else:
raiseOSError(osLastError())
elif defined(posix):
if f.handle == -1:
raise newException(IOError,
"Cannot resize MemFile opened with allowRemap=false")
if newFileSize != f.size:
let e = setFileSize(f.handle.FileHandle, newFileSize, f.size)
if e != 0.OSErrorCode: raiseOSError(e)
when defined(linux): #Maybe NetBSD, too?
# On Linux this can be over 100 times faster than a munmap,mmap cycle.
proc mremap(old: pointer; oldSize, newSize: csize_t; flags: cint):
pointer {.importc: "mremap", header: "<sys/mman.h>".}
let newAddr = mremap(f.mem, csize_t(f.size), csize_t(newFileSize), 1.cint)
if newAddr == cast[pointer](MAP_FAILED):
raiseOSError(osLastError())
else:
if munmap(f.mem, f.size) != 0:
raiseOSError(osLastError())
let newAddr = mmap(nil, newFileSize, PROT_READ or PROT_WRITE,
f.flags, f.handle, 0)
if newAddr == cast[pointer](MAP_FAILED):
raiseOSError(osLastError())
f.mem = newAddr
f.size = newFileSize
else:
raiseOSError(osLastError())
elif defined(posix):
if f.handle == -1:
raise newException(IOError,
"Cannot resize MemFile opened with allowRemap=false")
if newFileSize != f.size:
let e = setFileSize(f.handle.FileHandle, newFileSize, f.size)
if e != 0.OSErrorCode: raiseOSError(e)
when defined(linux): #Maybe NetBSD, too?
# On Linux this can be over 100 times faster than a munmap,mmap cycle.
proc mremap(old: pointer; oldSize, newSize: csize_t; flags: cint):
pointer {.importc: "mremap", header: "<sys/mman.h>".}
let newAddr = mremap(f.mem, csize_t(f.size), csize_t(newFileSize), 1.cint)
if newAddr == cast[pointer](MAP_FAILED):
raiseOSError(osLastError())
else:
if munmap(f.mem, f.size) != 0:
raiseOSError(osLastError())
let newAddr = mmap(nil, newFileSize, PROT_READ or PROT_WRITE,
f.flags, f.handle, 0)
if newAddr == cast[pointer](MAP_FAILED):
raiseOSError(osLastError())
f.mem = newAddr
f.size = newFileSize
proc close*(f: var MemFile) =
## closes the memory mapped file `f`. All changes are written back to the
@@ -389,6 +500,8 @@ proc close*(f: var MemFile) =
f.fHandle = INVALID_HANDLE_VALUE
if error:
lastErr = osLastError()
elif nimUseFallBack:
closeFallback(f)
else:
error = munmap(f.mem, f.size) != 0
lastErr = osLastError()

View File

@@ -175,23 +175,48 @@ proc parseEscapedUTF16*(buf: cstring, pos: var int): int =
else:
return -1
proc addSpan(dst: var string; src: string; startPos, endPos: int) {.inline.} =
let n = endPos - startPos
if n <= 0:
return
let old = dst.len
dst.setLen old + n
template impl =
for i in 0..<n:
dst[old + i] = src[startPos + i]
when nimvm:
impl
else:
when defined(js) or defined(nimscript):
impl
else:
{.noSideEffect.}:
copyMem dst[old].addr, src[startPos].unsafeAddr, n
proc parseString(my: var JsonParser): TokKind =
result = tkString
var pos = my.bufpos + 1
var spanStart = pos
if my.rawStringLiterals:
add(my.a, '"')
while true:
case my.buf[pos]
of '\0':
my.err = errQuoteExpected
my.err = errInvalidToken
addSpan(my.a, my.buf, spanStart, pos)
result = tkError
break
of '"':
addSpan(my.a, my.buf, spanStart, pos)
if my.rawStringLiterals:
add(my.a, '"')
inc(pos)
break
of '\\':
addSpan(my.a, my.buf, spanStart, pos)
if my.rawStringLiterals:
add(my.a, '\\')
case my.buf[pos+1]
@@ -251,14 +276,18 @@ proc parseString(my: var JsonParser): TokKind =
# don't bother with the error
add(my.a, my.buf[pos])
inc(pos)
spanStart = pos
of '\c':
addSpan(my.a, my.buf, spanStart, pos)
pos = lexbase.handleCR(my, pos)
add(my.a, '\c')
spanStart = pos
of '\L':
addSpan(my.a, my.buf, spanStart, pos)
pos = lexbase.handleLF(my, pos)
add(my.a, '\L')
spanStart = pos
else:
add(my.a, my.buf[pos])
inc(pos)
my.bufpos = pos # store back

View File

@@ -487,11 +487,18 @@ func `/`*(x: Uri, path: string): Uri =
func `?`*(u: Uri, query: openArray[(string, string)]): Uri =
## Concatenates the query parameters to the specified URI object.
## If the URI already has a query string, the new parameters are appended.
runnableExamples:
let foo = parseUri("https://example.com") / "foo" ? {"bar": "qux"}
assert $foo == "https://example.com/foo?bar=qux"
let bar = parseUri("https://example.com/foo?existing=1") ? {"bar": "qux"}
assert $bar == "https://example.com/foo?existing=1&bar=qux"
result = u
result.query = encodeQuery(query)
let newQuery = encodeQuery(query)
if newQuery.len > 0:
if result.query.len > 0:
result.query.add('&')
result.query.add(newQuery)
func `$`*(u: Uri): string =
## Returns the string representation of the specified URI object.

View File

@@ -54,7 +54,12 @@ type
typeOfProc, ## Prefer the interpretation that means `x` is a proc call.
typeOfIter ## Prefer the interpretation that means `x` is an iterator call.
proc typeof*(x: untyped; mode = typeOfIter): typedesc {.
TypeOfModifiers* = enum ## Modes to handle type modifiers `var`, `sink` and `lent`.
CompatibleTypeModifiers, ## Remove or keep type modifiers in the same way as old typeof. That means keep `sink` but remove `var` and `lent`.
RemoveTypeModifiers, ## Remove type modifiers.
KeepTypeModifiers, ## Keep type modifiers.
proc typeof*(x: untyped; mode = typeOfIter; modifierMode = CompatibleTypeModifiers): typedesc {.
magic: "TypeOf", noSideEffect, compileTime.} =
## Builtin `typeof` operation for accessing the type of an expression.
## Since version 0.20.0.
@@ -76,6 +81,11 @@ proc typeof*(x: untyped; mode = typeOfIter): typedesc {.
# since `typeOfProc` expects a typed expression and `myFoo2()` can
# only be used in a `for` context.
proc varParam(x: var int;
y: typeof(x, modifierMode = RemoveTypeModifiers);
z: typeof(x, modifierMode = KeepTypeModifiers)) = discard
doAssert varParam is proc (x: var int; y: int; z: var int) {.nimcall.}
proc `or`*(a, b: typedesc): typedesc {.magic: "TypeTrait", noSideEffect.}
## Constructs an `or` meta class.
@@ -1713,11 +1723,18 @@ when not (notJSnotNims and defined(nimSeqsV2)):
let ns = cast[NimString](s)
if ns == nil: nil
else: cast[ptr UncheckedArray[char]](addr ns.data[start])
template readRawDataStable*(s: var string; start = 0): ptr UncheckedArray[char] =
## Same as `readRawData` here: the data lives in a heap `NimStringDesc` at a
## stable address, so the pointer already survives moves of `s`. Takes `s` by
## `var` to match the `--strings:sso` version, so code can prepare for that
## upgrade without `when declared` guards.
readRawData(s, start)
else:
# JS/nimscript: callers are guarded by whenNotVmJsNims/when not defined(js)
proc beginStore*(s: var string; newLen: int; start = 0): ptr UncheckedArray[char] {.inline, noSideEffect, raises: [], tags: [].} = nil
proc endStore*(s: var string) {.inline, noSideEffect, raises: [], tags: [].} = discard
template readRawData*(s: string; start = 0): ptr UncheckedArray[char] = nil
template readRawDataStable*(s: var string; start = 0): ptr UncheckedArray[char] = nil
when not defined(js):
template newSeqImpl(T, len) =
@@ -3121,10 +3138,7 @@ when notJSnotNims:
not defined(nuttx) and
hostOS != "any"
proc raiseEIO(msg: string) {.noinline, noreturn.} =
raise newException(IOError, msg)
proc echoBinSafe(args: openArray[string]) {.compilerproc.} =
proc echoBinSafe(args: openArray[string]) {.compilerproc, raises: [].} =
when defined(androidNDK):
# When running nim in android app, stdout goes nowhere, so echo gets ignored
# To redirect echo to the android logcat, use -d:androidNDK
@@ -3146,7 +3160,7 @@ when notJSnotNims:
for s in args:
when defined(windows):
# equivalent to syncio.writeWindows
proc writeWindows(f: CFilePtr; s: string; doRaise = false) =
proc writeWindows(f: CFilePtr; s: string) =
# Don't ask why but the 'printf' family of function is the only thing
# that writes utf-8 strings reliably on Windows. At least on my Win 10
# machine. We also enable `setConsoleOutputCP(65001)` now by default.
@@ -3157,13 +3171,11 @@ when notJSnotNims:
if s[i] == '\0':
let w = c_fputc('\0', f)
if w != 0:
if doRaise: raiseEIO("cannot write string to file")
break
inc i
else:
let w = c_fprintf(f, "%s", unsafeAddr s[i])
if w <= 0:
if doRaise: raiseEIO("cannot write string to file")
break
inc i, w
writeWindows(cstdout, s)

View File

@@ -3,7 +3,7 @@
when defined(nimPreviewSlimSystem):
import std/assertions
when not defined(nimNoLentIterators):
when (not defined(nimNoLentIterators)) and not defined(js) and not defined(nimscript):
template lent2(T): untyped = lent T
else:
template lent2(T): untyped = T
@@ -37,7 +37,7 @@ iterator mitems*[T](a: var openArray[T]): var T {.inline.} =
yield a[i]
unCheckedInc(i)
iterator items*[IX, T](a: array[IX, T]): T {.inline.} =
iterator items*[IX, T](a: array[IX, T]): lent2 T {.inline.} =
## Iterates over each item of `a`.
when a.len > 0:
var i = low(IX)

View File

@@ -1,13 +1,13 @@
when notJSnotNims:
proc zeroMem*(p: pointer, size: Natural) {.inline, noSideEffect,
tags: [], raises: [], enforceNoRaises.}
proc zeroMem*(p: pointer, size: Natural) {.inline, gcsafe,
tags: [], raises: [], enforceNoRaises, noSideEffect.}
## Overwrites the contents of the memory at `p` with the value 0.
##
## Exactly `size` bytes will be overwritten. Like any procedure
## dealing with raw memory this is **unsafe**.
proc copyMem*(dest, source: pointer, size: Natural) {.inline, gcsafe,
tags: [], raises: [], enforceNoRaises.}
tags: [], raises: [], enforceNoRaises, noSideEffect.}
## Copies the contents from the memory at `source` to the memory
## at `dest`.
## Exactly `size` bytes will be copied. The memory
@@ -15,7 +15,7 @@ when notJSnotNims:
## memory this is **unsafe**.
proc moveMem*(dest, source: pointer, size: Natural) {.inline, gcsafe,
tags: [], raises: [], enforceNoRaises.}
tags: [], raises: [], enforceNoRaises, noSideEffect.}
## Copies the contents from the memory at `source` to the memory
## at `dest`.
##
@@ -24,8 +24,8 @@ when notJSnotNims:
## and is thus somewhat more safe than `copyMem`. Like any procedure
## dealing with raw memory this is still **unsafe**, though.
proc equalMem*(a, b: pointer, size: Natural): bool {.inline, noSideEffect,
tags: [], raises: [], enforceNoRaises.}
proc equalMem*(a, b: pointer, size: Natural): bool {.inline, gcsafe,
tags: [], raises: [], enforceNoRaises, noSideEffect.}
## Compares the memory blocks `a` and `b`. `size` bytes will
## be compared.
##
@@ -33,8 +33,8 @@ when notJSnotNims:
## otherwise. Like any procedure dealing with raw memory this is
## **unsafe**.
proc cmpMem*(a, b: pointer, size: Natural): int {.inline, noSideEffect,
tags: [], raises: [], enforceNoRaises.}
proc cmpMem*(a, b: pointer, size: Natural): int {.inline, gcsafe,
tags: [], raises: [], enforceNoRaises, noSideEffect.}
## Compares the memory blocks `a` and `b`. `size` bytes will
## be compared.
##

View File

@@ -91,7 +91,10 @@ else:
elif defined(gcMarkAndSweep):
# XXX use 'compileOption' here
include "system/gc_ms"
else:
elif not (defined(nimV2) or usesDestructors):
# equivalent to a plain `else` here, but spelled out so that the IC
# dependency scanner (which sees `else` imports/includes unguarded)
# doesn't schedule system/gc's transitive imports under --mm:orc
include "system/gc"
when not declared(nimNewSeqOfCap) and not defined(nimSeqsV2):

View File

@@ -40,7 +40,8 @@ type
wasm32, ## WASM, 32-bit
e2k, ## MCST Elbrus 2000
loongarch64, ## LoongArch 64-bit processor
s390x ## IBM Z
s390x, ## IBM Z
wasm64 ## WASM, 64-bit
OsPlatform* {.pure.} = enum ## the OS this program will run on.
none, dos, windows, os2, linux, morphos, skyos, solaris,
@@ -101,5 +102,6 @@ const
elif defined(e2k): CpuPlatform.e2k
elif defined(loongarch64): CpuPlatform.loongarch64
elif defined(s390x): CpuPlatform.s390x
elif defined(wasm64): CpuPlatform.wasm64
else: CpuPlatform.none
## the CPU this program will run on.

View File

@@ -261,4 +261,14 @@ template readRawData*(s: string; start = 0): ptr UncheckedArray[char] =
## Template ensures no copy of `s`; ptr is valid while `s` is alive.
rawDataImpl(cast[ptr NimStringV2](unsafeAddr s), start)
template readRawDataStable*(s: var string; start = 0): ptr UncheckedArray[char] =
## Like `readRawData`, but the returned pointer additionally survives moves and
## copies of `s` (while `s` stays alive and is not reassigned). For this string
## implementation the char data already lives in a heap payload at an address
## independent of the `string` value itself, so no promotion is needed and this
## is identical to `readRawData`. Takes `s` by `var` to match the `--strings:sso`
## version (which promotes a small inline string to the heap), so code written
## against `readRawDataStable` compiles unchanged under either implementation.
rawDataImpl(cast[ptr NimStringV2](addr s), start)
{.pop.}

View File

@@ -770,6 +770,33 @@ template readRawData*(s: string; start = 0): ptr UncheckedArray[char] =
## Template ensures no copy of `s` is made; ptr is valid while `s` is alive.
rawDataImpl(cast[ptr SmallString](unsafeAddr s), start)
proc readRawDataStable*(s: var string; start = 0): ptr UncheckedArray[char] {.inline.} =
## Like `readRawData`, but the returned pointer stays valid across moves and
## copies of `s` (as long as `s` stays alive and is not reassigned). A
## short/medium string keeps its chars *inline* in the string object, so a
## plain `readRawData` pointer dangles the moment the object is moved; this
## promotes `s` to its heap (long) representation first, whose payload address
## is independent of where the string object itself lives. Use this whenever an
## interior pointer must outlive the current scope of the owning string (e.g.
## a cursor cached alongside the buffer it points into).
let ss = cast[ptr SmallString](addr s)
let slen = ssLen(ss[])
if slen > 0 and slen <= PayloadSize:
# Promote inline/medium to a long heap block so the payload lives at a
# stable address. Mirrors the short/medium -> long transition in `add`.
let newCap = max(slen, resize(slen))
let p = cast[ptr LongString](alloc(LongStringDataOffset + newCap + 1))
p.rc = 1
p.fullLen = slen
p.capImpl = newCap
copyMem(addr p.data[0], inlinePtr(ss[]), slen)
p.data[slen] = '\0'
ss[].more = p
setSSLen(ss[], HeapSlen)
# Hot-prefix cache (bytes 1..AlwaysAvail) already mirrors data[0..AlwaysAvail-1]
# because setSSLen only rewrote byte 0; the inline chars are untouched.
rawDataImpl(ss, start)
# These take `string` (tyString) so the codegen uses them directly, bypassing
# strmantle.nim's versions which go through nimStrLen/nimStrAtMutV3 compilerproc calls.
proc cmpStrings(a, b: string): int {.compilerproc, inline.} =

View File

@@ -1,260 +0,0 @@
#
# Nim's Runtime Library
# (c) Copyright 2026 Nim Contributors
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Wrapper for the 8-bit PCRE2 API.
when sizeof(int) == 4:
const ANCHORED* = low(int)
else:
const ANCHORED* = int(0x80000000)
const
NO_UTF_CHECK* = int(0x40000000)
ENDANCHORED* = int(0x20000000)
const
ALLOW_EMPTY_CLASS* = 0x00000001
ALT_BSUX* = 0x00000002
AUTO_CALLOUT* = 0x00000004
CASELESS* = 0x00000008
DOLLAR_ENDONLY* = 0x00000010
DOTALL* = 0x00000020
DUPNAMES* = 0x00000040
EXTENDED* = 0x00000080
FIRSTLINE* = 0x00000100
MATCH_UNSET_BACKREF* = 0x00000200
MULTILINE* = 0x00000400
NEVER_UCP* = 0x00000800
NEVER_UTF* = 0x00001000
NO_AUTO_CAPTURE* = 0x00002000
NO_AUTO_POSSESS* = 0x00004000
NO_DOTSTAR_ANCHOR* = 0x00008000
NO_START_OPTIMIZE* = 0x00010000
NO_START_OPTIMISE* = NO_START_OPTIMIZE
UCP* = 0x00020000
UNGREEDY* = 0x00040000
UTF* = 0x00080000
UTF8* = UTF
NEVER_BACKSLASH_C* = 0x00100000
ALT_CIRCUMFLEX* = 0x00200000
ALT_VERBNAMES* = 0x00400000
USE_OFFSET_LIMIT* = 0x00800000
EXTENDED_MORE* = 0x01000000
LITERAL* = 0x02000000
MATCH_INVALID_UTF* = 0x04000000
ALT_EXTENDED_CLASS* = 0x08000000
## PCRE2 no longer exposes PCRE's `JAVASCRIPT_COMPAT` option. `ALT_BSUX`
## preserves the most important JavaScript-style escape handling.
JAVASCRIPT_COMPAT* = ALT_BSUX
const
JIT_COMPLETE* = 0x00000001
JIT_PARTIAL_SOFT* = 0x00000002
JIT_PARTIAL_HARD* = 0x00000004
JIT_INVALID_UTF* = 0x00000100
JIT_TEST_ALLOC* = 0x00000200
const
NOTBOL* = 0x00000001
NOTEOL* = 0x00000002
NOTEMPTY* = 0x00000004
NOTEMPTY_ATSTART* = 0x00000008
PARTIAL_SOFT* = 0x00000010
PARTIAL_HARD* = 0x00000020
DFA_RESTART* = 0x00000040
DFA_SHORTEST* = 0x00000080
NO_JIT* = 0x00002000
COPY_MATCHED_SUBJECT* = 0x00004000
DISABLE_RECURSELOOP_CHECK* = 0x00040000
const
NEWLINE_CR* = 1
NEWLINE_LF* = 2
NEWLINE_CRLF* = 3
NEWLINE_ANY* = 4
NEWLINE_ANYCRLF* = 5
NEWLINE_NUL* = 6
BSR_UNICODE* = 1
BSR_ANYCRLF* = 2
const
ERROR_NOMATCH* = -1
ERROR_PARTIAL* = -2
ERROR_UTF8_ERR1* = -3
ERROR_UTF8_ERR2* = -4
ERROR_UTF8_ERR3* = -5
ERROR_UTF8_ERR4* = -6
ERROR_UTF8_ERR5* = -7
ERROR_UTF8_ERR6* = -8
ERROR_UTF8_ERR7* = -9
ERROR_UTF8_ERR8* = -10
ERROR_UTF8_ERR9* = -11
ERROR_UTF8_ERR10* = -12
ERROR_UTF8_ERR11* = -13
ERROR_UTF8_ERR12* = -14
ERROR_UTF8_ERR13* = -15
ERROR_UTF8_ERR14* = -16
ERROR_UTF8_ERR15* = -17
ERROR_UTF8_ERR16* = -18
ERROR_UTF8_ERR17* = -19
ERROR_UTF8_ERR18* = -20
ERROR_UTF8_ERR19* = -21
ERROR_UTF8_ERR20* = -22
ERROR_UTF8_ERR21* = -23
ERROR_BADDATA* = -29
ERROR_MIXEDTABLES* = -30
ERROR_BADMAGIC* = -31
ERROR_BADMODE* = -32
ERROR_BADOFFSET* = -33
ERROR_BADOPTION* = -34
ERROR_BADREPLACEMENT* = -35
ERROR_BADUTFOFFSET* = -36
ERROR_CALLOUT* = -37
ERROR_INTERNAL* = -44
ERROR_JIT_BADOPTION* = -45
ERROR_JIT_STACKLIMIT* = -46
ERROR_MATCHLIMIT* = -47
ERROR_NOMEMORY* = -48
ERROR_NOSUBSTRING* = -49
ERROR_NULL* = -51
ERROR_RECURSELOOP* = -52
ERROR_DEPTHLIMIT* = -53
ERROR_RECURSIONLIMIT* = ERROR_DEPTHLIMIT
ERROR_UNAVAILABLE* = -54
ERROR_UNSET* = -55
ERROR_BADOFFSETLIMIT* = -56
ERROR_HEAPLIMIT* = -63
ERROR_DFA_UINVALID_UTF* = -66
ERROR_INVALIDOFFSET* = -67
ERROR_JIT_UNSUPPORTED* = -68
const
INFO_ALLOPTIONS* = 0
INFO_ARGOPTIONS* = 1
INFO_BACKREFMAX* = 2
INFO_BSR* = 3
INFO_CAPTURECOUNT* = 4
INFO_FIRSTCODEUNIT* = 5
INFO_FIRSTCODETYPE* = 6
INFO_FIRSTBITMAP* = 7
INFO_HASCRORLF* = 8
INFO_JCHANGED* = 9
INFO_JITSIZE* = 10
INFO_LASTCODEUNIT* = 11
INFO_LASTCODETYPE* = 12
INFO_MATCHEMPTY* = 13
INFO_MATCHLIMIT* = 14
INFO_MAXLOOKBEHIND* = 15
INFO_MINLENGTH* = 16
INFO_NAMECOUNT* = 17
INFO_NAMEENTRYSIZE* = 18
INFO_NAMETABLE* = 19
INFO_NEWLINE* = 20
INFO_DEPTHLIMIT* = 21
INFO_RECURSIONLIMIT* = INFO_DEPTHLIMIT
INFO_SIZE* = 22
INFO_HASBACKSLASHC* = 23
INFO_FRAMESIZE* = 24
INFO_HEAPLIMIT* = 25
INFO_EXTRAOPTIONS* = 26
const
CONFIG_BSR* = 0
CONFIG_JIT* = 1
CONFIG_JITTARGET* = 2
CONFIG_LINKSIZE* = 3
CONFIG_MATCHLIMIT* = 4
CONFIG_NEWLINE* = 5
CONFIG_PARENSLIMIT* = 6
CONFIG_DEPTHLIMIT* = 7
CONFIG_RECURSIONLIMIT* = CONFIG_DEPTHLIMIT
CONFIG_STACKRECURSE* = 8
CONFIG_UNICODE* = 9
CONFIG_UNICODE_VERSION* = 10
CONFIG_VERSION* = 11
CONFIG_HEAPLIMIT* = 12
CONFIG_NEVER_BACKSLASH_C* = 13
CONFIG_COMPILED_WIDTHS* = 14
CONFIG_TABLES_LENGTH* = 15
const
ZERO_TERMINATED* = not 0.csize_t
UNSET* = not 0.csize_t
type
Pcre* = object
CompileContext* = object
GeneralContext* = object
MatchContext* = object
MatchData* = object
JitStack* = object
when not defined(usePcreHeader):
when hostOS == "windows":
const pcre2Dll = "pcre2-8.dll"
elif hostOS == "macosx":
const pcre2Dll = "libpcre2-8(.0|).dylib"
else:
const pcre2Dll = "libpcre2-8.so(.0|)"
{.push dynlib: pcre2Dll.}
else:
{.passC: "-DPCRE2_CODE_UNIT_WIDTH=8".}
{.push header: "<pcre2.h>".}
{.push cdecl, importc: "pcre2_$1_8".}
proc compile*(pattern: ptr uint8,
length: csize_t,
options: uint32,
errorCode: ptr cint,
errorOffset: ptr csize_t,
context: ptr CompileContext): ptr Pcre
proc code_free*(code: ptr Pcre)
proc config*(what: uint32,
where: pointer): cint
proc get_error_message*(errorCode: cint,
buffer: ptr uint8,
bufferLength: csize_t): cint
proc match*(code: ptr Pcre,
subject: ptr uint8,
length: csize_t,
startOffset: csize_t,
options: uint32,
matchData: ptr MatchData,
context: ptr MatchContext): cint
proc match_data_create*(oveccount: uint32,
context: ptr GeneralContext): ptr MatchData
proc match_data_create_from_pattern*(code: ptr Pcre,
context: ptr GeneralContext): ptr MatchData
proc match_data_free*(matchData: ptr MatchData)
proc get_ovector_pointer*(matchData: ptr MatchData): ptr csize_t
proc get_ovector_count*(matchData: ptr MatchData): uint32
proc pattern_info*(code: ptr Pcre,
what: uint32,
where: pointer): cint
proc jit_compile*(code: ptr Pcre,
options: uint32): cint
proc jit_free_unused_memory*()
{.pop.}
{.pop.}

View File

@@ -2,6 +2,7 @@
[![Build Status](https://dev.azure.com/nim-lang/Nim/_apis/build/status/nim-lang.Nim?branchName=devel)](https://dev.azure.com/nim-lang/Nim/_build/latest?definitionId=1&branchName=devel)
This repository contains the Nim compiler, Nim's stdlib, tools, and documentation.
For more information about Nim, including downloads and documentation for
the latest release, check out [Nim's website][nim-site] or [bleeding edge docs](https://nim-lang.github.io/Nim/).

View File

@@ -158,6 +158,7 @@ pkg "sim"
pkg "smtp", "nimble compileExample"
pkg "snip", "nimble test", "https://github.com/genotrance/snip"
pkg "ssostrings", "nim c -r tests/tssostrings.nim"
pkg "ssz_serialization", "nim c -r tests/test_all.nim"
pkg "stew"
pkg "stint", "nimble test_internal"
pkg "strslice"

19
tests/arc/t19312.nim Normal file
View File

@@ -0,0 +1,19 @@
discard """
matrix: "--mm:orc"
output: '''(val: 1)
(val: 1)'''
"""
# Issue #19312: copied ref object is converted to nil if not used in declaration module under ARC/ORC
# https://github.com/nim-lang/Nim/issues/19312
type
Wrapper* = object
val: int
RefWrapper* = ref Wrapper
let
a* = RefWrapper(val: 1)
b* = a
echo b[]
echo a[]

43
tests/arc/t25595.nim Normal file
View File

@@ -0,0 +1,43 @@
discard """
matrix: "--mm:orc; --mm:arc; --mm:refc"
"""
# bug #25595: cursor inference must not borrow a case object whose source can be
# mutated through the cursor's own ref across a call. `let c = h.w` was inferred as a
# non-owning cursor; `clear(c.r)` overwrites `h.w` via the cursor's back-reference,
# freeing the ref while the borrow still uses it -> use-after-free. Detected here
# deterministically: the element's destructor must not run during the call.
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()
doAssert not destroyed, "use-after-free: element destroyed during the call"
result = o.value
proc go(h: H): int =
let c = 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()

47
tests/arc/t25850.nim Normal file
View File

@@ -0,0 +1,47 @@
discard """
cmd: '''nim c --mm:orc --expandArc:uIf --expandArc:uCase $file'''
nimout: '''
--expandArc: uIf
block :tmp:
let s = w()
if true:
r[] = s
else:
r[] = s
-- end of expandArc ------------------------
--expandArc: uCase
block :tmp:
let s = w()
case n
of 0:
r[] = s
else:
r[] = w()
-- end of expandArc ------------------------
'''
"""
# bug #25850
# Assigning an expression-based control flow construct (an `if`/`case` nested in
# a `block`) must distribute the assignment directly into the leaf branches
# instead of creating redundant intermediate temporaries per branch.
proc w(): array[1000, byte] {.noinline.} = discard
proc uIf(r: ptr array[1000, byte]) =
r[] = (block:
let s = w()
if true: s else: s)
proc uCase(r: ptr array[1000, byte], n: int) =
r[] = (block:
let s = w()
case n
of 0: s
else: w())
var d: array[1000, byte]
uIf(addr d)
uCase(addr d, 0)

17
tests/async/t16416.nim Normal file
View File

@@ -0,0 +1,17 @@
discard """
output: '''done'''
"""
# Issue #16416: Can't call closure iterator from inside an async function
# https://github.com/nim-lang/Nim/issues/16416
import asyncdispatch
iterator x(): int {.closure.} =
yield 1
proc y() {.async.} =
for z in x():
discard
waitFor y()
echo "done"

View File

@@ -0,0 +1,36 @@
discard """
matrix: "; --panics:on"
"""
# issue #25851: --panics:on must not drop the nimErr_ check after a closure
# call whose result is consumed directly (e.g. `result.add elem(src)`).
# Regression from #25295.
type
Overrun = object of CatchableError
Source = object
data: seq[bool]
cursor: int
ElemFn = proc(src: var Source): bool {.closure.}
proc drawBool(src: var Source): bool =
if src.cursor >= src.data.len: raise newException(Overrun, "exhausted")
result = src.data[src.cursor]; inc src.cursor
proc listRun(elem: ElemFn, src: var Source): seq[bool] =
result = @[]
while true:
if not src.drawBool(): break
result.add elem(src) # closure call the result flows straight
# into `add`, which previously caused the
# compiler to skip the nimErr_ check.
let elem: ElemFn = proc(src: var Source): bool = src.drawBool()
# Both --panics:on and --panics:off must propagate the Overrun.
var caught = false
try:
var src = Source(data: @[true])
discard listRun(elem, src)
except Overrun:
caught = true
doAssert caught, "Overrun exception was swallowed"

View File

@@ -80,3 +80,20 @@ block tissue7104:
sp do ():
inc i
echo "ok ", i
block: # bug #25903
iterator g: int {.closure.} =
discard try:
yield 0
0
except IOError, OSError:
0
let _ = g
block: # bug #25904
iterator w: int {.closure.} =
discard try: 0
except IOError, OSError:
yield 0
0
let _ = w

View File

@@ -1,7 +1,7 @@
discard """
targets: "c"
matrix: "--debugger:native --mangle:nim"
ccodecheck: "'testFunc__titaniummangle95nim_u'"
ccodecheck: "'testFunc_u' \\d+ '__titaniummangle95nim'"
"""
#When debugging this notice that if one check fails, it can be due to any of the above.

View File

@@ -0,0 +1,28 @@
discard """
action: "reject"
errormsg: "type mismatch"
"""
type
Dollarable = concept
proc `$`(x: Self): string
proc checkEqual(x, y: Dollarable) =
if x != y:
echo $x
echo $y
type
StateFlags = enum
sfMatch
sfSoft
MatchKind = enum
NoFurtherMatch
NoMatch
Match
AllFurtherMatch
proc `==`(a: set[StateFlags]; b: MatchKind): bool = true
checkEqual({sfMatch, sfSoft}, Match)

23
tests/concepts/t14913.nim Normal file
View File

@@ -0,0 +1,23 @@
discard """
output: '''done'''
"""
# Issue #14913: Compiler crash when using a default parameter value for a parameter whose type is a concept
# https://github.com/nim-lang/Nim/issues/14913
type
State = object
MoreState = object
StringRecord = concept x, type T
for k, v in fieldPairs(x):
k is string
v is string
StateStrings = object
a, b: string
proc combine(a: State, b: MoreState): StateStrings = discard
proc whoops[T: StringRecord](a: State, b: MoreState, c: T = a.combine(b)) =
discard
whoops(State(), MoreState())
echo "done"

View File

@@ -14,6 +14,8 @@ b
c
1
2
5
test
'''
"""
import conceptsv2_helper
@@ -600,3 +602,15 @@ block:
let test = MemMapFileStream()
spring(test)
# explicit negative "bind once"
type
Dollarable = concept
proc `$`(x: Self): string
proc checkEqual2[T: Dollarable; S: Dollarable](x: T, y: S) =
echo $x
echo $y
checkEqual2(5, "test")

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